curl --request GET \
--url https://api.ctrl-hub.com/v3/competency-expiring-records \
--header 'X-Session-Token: <api-key>'import requests
url = "https://api.ctrl-hub.com/v3/competency-expiring-records"
headers = {"X-Session-Token": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-Session-Token': '<api-key>'}};
fetch('https://api.ctrl-hub.com/v3/competency-expiring-records', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ctrl-hub.com/v3/competency-expiring-records",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-Session-Token: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.ctrl-hub.com/v3/competency-expiring-records"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-Session-Token", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.ctrl-hub.com/v3/competency-expiring-records")
.header("X-Session-Token", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ctrl-hub.com/v3/competency-expiring-records")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-Session-Token"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "competency-expiring-records",
"attributes": {
"record_type": "qualification",
"definition": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>"
},
"expires_at": "2023-11-07T05:31:56Z",
"risk_status": "none",
"required_by": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>"
}
]
},
"relationships": {
"user": {
"data": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "users"
}
}
}
}
],
"meta": {
"pagination": {
"counts": {
"pages": 1,
"resources": 1
},
"current_page": 1,
"offsets": {
"next": null,
"previous": null
},
"requested": {
"limit": 10,
"offset": 0
}
},
"features": {
"include": {
"options": [
"related.resource"
]
}
}
},
"jsonapi": {
"version": "1.0"
},
"included": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"type": "users",
"attributes": {
"email": "john.doe@example.com",
"profile": {
"work": {
"occupation": "Site Manager",
"cscs": "CSC123456",
"eusr": "EUR789123",
"start_date": "2020-03-01T00:00:00.000Z"
},
"personal": {
"first_name": "John",
"last_name": "Doe",
"dob": "1985-06-15T00:00:00.000Z",
"username": "johndoe"
},
"contact": {
"mobile": "+44 7700 900123",
"landline": "+44 20 7946 0958"
},
"address": {
"name": "Tech Tower",
"number": "42",
"street": "High Street",
"area": "Westminster",
"town": "London",
"county": "Greater London",
"postcode": "SW1A 1AA",
"country_code": "GB",
"what3words": "filled.count.soap"
},
"settings": {
"preferred_language": "en-GB",
"timezone": "Europe/London"
}
},
"status": "active",
"password_set_at": "2026-09-25T10:15:00.000Z",
"mfa_enabled_at": "2026-10-06T09:30:00.000Z",
"identities": [
{
"id": "39ce13b8-1116-416a-ad5f-3c5edfd44f53",
"platform": "multi_tenant",
"meta": {}
}
]
},
"meta": {
"managed_type": "none",
"withheld_organisations": [
"c000c344-8847-47da-a091-32e75902d3b1"
]
},
"relationships": {
"organisations": {
"data": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "organisations"
}
]
},
"teams": {
"data": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "teams"
}
]
},
"managing_organisation": {
"data": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "organisations"
}
}
}
}
]
}{
"id": "98ca4a78-b66f-4234-9719-aaf832ee6669",
"status": "400",
"title": "A validation error was encountered",
"source": {
"parameter": "include"
},
"meta": {
"resource": "wrong_value"
}
}{
"id": "05fc9c8d-73b9-4697-9337-57f7a567a48f",
"status": "401",
"title": "You are not authorised to access this resource",
"detail": "In order to access this resource, you need the 'admin' role.",
"code": "AUTH.001"
}{
"id": "fe9d9a69-f0a7-4fdc-bb2c-176027f316c5",
"status": "500",
"title": "Internal Server Error",
"detail": "An unexpected error occurred on the server."
}List expiring competency records
List the competency records that expire, across every member of an organisation. One row per record: qualification awards, training, induction and assessment records, and skills-register, scheme and category registrations.
Only a person’s latest record for each definition is listed, so an expired record that has since been renewed does not appear. Exemption records and records with no expiry are never listed, but they still count as the latest record: a renewal with no expiry, or a later exemption, hides an older expiring record. Deactivated members are left out.
A qualification award is listed only when one of the person’s job roles requires it, which is how the holder detail page shows awards. The other record types are listed whether or not a role requires them.
The organisation filter is required. A caller reading a bridged
organisation sees only the people assigned to the roles shared with them, and
only the records those roles require.
Send format=csv for the export instead of a page. The export ignores
pagination and covers the whole filtered set, up to 10 000 rows.
Served by a hand-written handler rather than a generated one, because the resource is computed rather than stored and the operation negotiates CSV.
curl --request GET \
--url https://api.ctrl-hub.com/v3/competency-expiring-records \
--header 'X-Session-Token: <api-key>'import requests
url = "https://api.ctrl-hub.com/v3/competency-expiring-records"
headers = {"X-Session-Token": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-Session-Token': '<api-key>'}};
fetch('https://api.ctrl-hub.com/v3/competency-expiring-records', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ctrl-hub.com/v3/competency-expiring-records",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-Session-Token: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.ctrl-hub.com/v3/competency-expiring-records"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-Session-Token", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.ctrl-hub.com/v3/competency-expiring-records")
.header("X-Session-Token", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ctrl-hub.com/v3/competency-expiring-records")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-Session-Token"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "competency-expiring-records",
"attributes": {
"record_type": "qualification",
"definition": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>"
},
"expires_at": "2023-11-07T05:31:56Z",
"risk_status": "none",
"required_by": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>"
}
]
},
"relationships": {
"user": {
"data": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "users"
}
}
}
}
],
"meta": {
"pagination": {
"counts": {
"pages": 1,
"resources": 1
},
"current_page": 1,
"offsets": {
"next": null,
"previous": null
},
"requested": {
"limit": 10,
"offset": 0
}
},
"features": {
"include": {
"options": [
"related.resource"
]
}
}
},
"jsonapi": {
"version": "1.0"
},
"included": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"type": "users",
"attributes": {
"email": "john.doe@example.com",
"profile": {
"work": {
"occupation": "Site Manager",
"cscs": "CSC123456",
"eusr": "EUR789123",
"start_date": "2020-03-01T00:00:00.000Z"
},
"personal": {
"first_name": "John",
"last_name": "Doe",
"dob": "1985-06-15T00:00:00.000Z",
"username": "johndoe"
},
"contact": {
"mobile": "+44 7700 900123",
"landline": "+44 20 7946 0958"
},
"address": {
"name": "Tech Tower",
"number": "42",
"street": "High Street",
"area": "Westminster",
"town": "London",
"county": "Greater London",
"postcode": "SW1A 1AA",
"country_code": "GB",
"what3words": "filled.count.soap"
},
"settings": {
"preferred_language": "en-GB",
"timezone": "Europe/London"
}
},
"status": "active",
"password_set_at": "2026-09-25T10:15:00.000Z",
"mfa_enabled_at": "2026-10-06T09:30:00.000Z",
"identities": [
{
"id": "39ce13b8-1116-416a-ad5f-3c5edfd44f53",
"platform": "multi_tenant",
"meta": {}
}
]
},
"meta": {
"managed_type": "none",
"withheld_organisations": [
"c000c344-8847-47da-a091-32e75902d3b1"
]
},
"relationships": {
"organisations": {
"data": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "organisations"
}
]
},
"teams": {
"data": [
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "teams"
}
]
},
"managing_organisation": {
"data": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "organisations"
}
}
}
}
]
}{
"id": "98ca4a78-b66f-4234-9719-aaf832ee6669",
"status": "400",
"title": "A validation error was encountered",
"source": {
"parameter": "include"
},
"meta": {
"resource": "wrong_value"
}
}{
"id": "05fc9c8d-73b9-4697-9337-57f7a567a48f",
"status": "401",
"title": "You are not authorised to access this resource",
"detail": "In order to access this resource, you need the 'admin' role.",
"code": "AUTH.001"
}{
"id": "fe9d9a69-f0a7-4fdc-bb2c-176027f316c5",
"status": "500",
"title": "Internal Server Error",
"detail": "An unexpected error occurred on the server."
}Authorizations
Session token for authentication.
Query Parameters
Filters the records listed.
eq(organisation,xxx): Required. The organisation whose members' records are listed.ge(expires_at,xxx)andle(expires_at,xxx): the expiry window. Withoutge, records that have already expired are included.in(type,[xxx,yyy]): the record types. One ofqualification,training,induction,assessment,registration,scheme_registrationorcategory_registration.in(job_role,[xxx,yyy]): records required by at least one of these job roles.in(risk,[xxx,yyy]): the risk bands. One ofcritical,high,medium,lowornone.criticalmeans expired.contains(name,xxx): the person's name or email. Every word must match.
A filter term this endpoint does not recognise, or one given more than once, is rejected with a 400 rather than ignored. expires_at takes one ge and one le.
For more information on using named filters, see the docs
1The field to order the records by. One field only.
Defaults to expires_at, the soonest expiry first. risk orders by severity,
from critical down to none, and soonest expiry first within a band.
1expires_at, -expires_at, risk, -risk A comma separated list of related resources to include.
user Limit the number of resources returned by the API
x >= 1Offset the resources returned by the API
x >= 0JSON:API pagination, as page[limit] and page[offset]. The same as the top-level limit and offset parameters; when both forms are sent, page[...] wins.
Show child attributes
Show child attributes
Asks for the CSV export rather than a page of records. The export exists as a plain URL so a browser download can use it without setting a header.
csv Response
A page of expiring competency records, or the whole filtered set as CSV when
format=csv was asked for.
JSON API response object