API for rota and care systems

A rota or care management system can connect to My Care Academy to send a company's staff in as learners, assign their training, read back their training records and certificates, and be told when a certificate is issued or training goes overdue. MyRoster uses exactly this.

Getting a key

Each company connects its own systems. A manager opens Settings, Integration, Other systems and alerts in My Care Academy, makes a key for your system and gives it to you. A key belongs to one company: everything you send or read with it is that company's, and nobody else's. The manager can switch it off at any time. The API is part of the Enterprise package.

Send the key on every request:

Authorization: Bearer mca_…

All addresses below are under https://www.mycareacademy.co.uk/api/portal. Requests and answers are JSON. A wrong or switched-off key answers 401; a company whose package doesn't include the API, or that isn't active, answers 403.

Requests

POST/sync-learners

Send the company's staff, up to 1,000 at a time. Each person is matched by your id for them (externalRef), then by email address; someone new is sent an invitation to set a password. A person sent as archived is signed out and can't sign in. With updateOnly: true nobody new is invited: use it for automatic syncs, and leave it off when someone has asked for a person to be given an account.

{
  "learners": [
    { "externalRef": "carer-118", "email": "sam@example.com", "fullName": "Sam Jones", "status": "active" }
  ],
  "updateOnly": false
}

Every person sent gets an outcome, in the order sent:

{
  "results": [{ "externalRef": "carer-118", "outcome": "invited", "userId": "…" }],
  "counts": { "invited": 1 }
}

Outcomes: invited, adopted (already here, now matched to your id), updated, skipped_archived, skipped_not_on_platform (with updateOnly) and error, with the reason. The company's managers see the last sync, and anything that went wrong, in plain words on their settings page.

GET/catalogue

What can be assigned: the courses, grouped by topic, and the programmes made of them.

{
  "topics": [{ "id": "…", "title": "Safeguarding" }],
  "courses": [{ "id": "…", "topicId": "…", "title": "Safeguarding Adults", "summary": "…",
                "expiryMonths": 12, "estimatedMinutes": 45 }],
  "pathways": [{ "id": "…", "title": "Care Certificate", "kind": "…", "summary": "…", "courseIds": ["…"] }]
}

POST/assign

Assign courses or programmes to people, by your ids for them. Assigning something a person already has never resets their progress. Without a dueDate, new training is due at the end of this month.

{ "externalRefs": ["carer-118"], "courseIds": ["…"], "pathwayIds": [], "dueDate": "2026-12-31" }
{ "assigned": 3, "newlyAssigned": 1, "learners": 1, "courses": 3, "unknownRefs": [], "inactiveRefs": [] }

GET/records

Everyone matched to your system, with their training and certificates. Each certificate has a download link that works for an hour; ask again for a fresh one.

{
  "courses": [{ "id": "…", "title": "Safeguarding Adults", "expiryMonths": 12 }],
  "learners": [{
    "externalRef": "carer-118", "fullName": "Sam Jones", "email": "sam@example.com",
    "status": "active", "lastSeenAt": "2026-10-30T09:12:00Z",
    "enrolments": [{ "courseId": "…", "status": "in_progress", "progress": 40, "dueDate": "2026-12-31",
                     "completionCount": 0, "assignedAt": "…" }],
    "certificates": [{ "certificateNumber": "MCA-…", "courseId": "…", "issuedAt": "…",
                       "expiresAt": "…", "downloadUrl": "https://…" }]
  }]
}

Alerts

Instead of asking us over and over, your system can be told. The company's manager adds the address you give them, chooses the alerts, and gives you a signing secret (whsec_…). We then send a POST with a JSON body to that address. Answer with any 2xx within 10 seconds. Anything else is tried twice more straight away, then once a day for three days. The address must be https.

Every alert has the same outline:

{
  "id": "6f1c…",                     // the same on every retry: use it to ignore repeats
  "type": "certificate.issued",
  "createdAt": "2026-10-30T14:02:11Z",
  "organisation": { "id": "…", "name": "Example Care Ltd" },
  "data": { … }
}

certificate.issued

Someone has earned a certificate, online or in a classroom.

"data": {
  "certificate": { "id": "…", "number": "MCA-…", "issuedAt": "…", "expiresAt": "…",
                   "verifyUrl": "https://www.mycareacademy.co.uk/verify?number=MCA-…" },
  "learner": { "id": "…", "externalRef": "carer-118", "fullName": "Sam Jones", "email": "sam@example.com" },
  "course": { "id": "…", "title": "Safeguarding Adults" }
}

training.overdue

Once a day, every course that has gone past its due date since the last one: one alert listing them all, and none on a day with nothing new. Training that was already overdue before the alerts were set up is in GET /records.

"data": {
  "items": [{ "enrolmentId": "…", "dueDate": "2026-10-29",
              "learner": { "id": "…", "externalRef": "carer-118", "fullName": "Sam Jones", "email": "…" },
              "course": { "id": "…", "title": "Moving and Handling" } }]
}

ping

Sent when the manager presses “Send a test”.

Checking an alert came from us

Each alert has a MCA-Signature header, t=<unix seconds>,v1=<hex>, where the hex is the HMAC-SHA256, keyed with your signing secret, of the timestamp, a full stop, and the body exactly as it arrived. Refuse an alert whose signature doesn't match, or whose timestamp is more than five minutes out. The headers MCA-Event and MCA-Delivery carry the type and the alert's id.

import crypto from "node:crypto";

function fromMyCareAcademy(secret, rawBody, header) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Certificates

Anyone can check a certificate is genuine on our verification page, with the number printed on it.

Questions

If you're building a connection and something here isn't clear, or you need something it doesn't do, get in touch.