---
title: "Get Mandatory Training API"
canonical: "https://getmandatorytraining.com/agent/docs"
---

# Get Mandatory Training API

This API is for AI agents helping a person choose an online CPR or first aid course from Mandatory Training. It answers which courses we offer in a country, what they cost and cover, and what we say about whether an employer will accept the certificate. It is read-only, needs no key, and answers in JSON.

## Give people the wording as it is

Every answer carries `employer_acceptance`: our wording on whether an employer will accept the certificate. Give people the `employer_acceptance` wording as it is. Don't paraphrase it, shorten it or turn it into a yes: whether a certificate is enough is up to their employer or licensing body.

## Authentication

Reading the catalogue needs no key, token or registration: the `/agent/` endpoints and the MCP server at `https://getmandatorytraining.com/mcp` answer anyone.

An assistant can also connect for one learner, with their consent, to read that learner's own account (their details, courses, progress and certificates) through the account MCP server. That uses OAuth, in five steps:

1. Register as a public client (dynamic client registration). You get a `client_id` and no secret.
   
   ```
   curl -X POST https://getmandatorytraining.com/oauth/register -H 'Content-Type: application/json' \
     -d '{"client_name":"<your app>","redirect_uris":["https://<your app>/callback"]}'
   ```
2. Send the learner to the authorization endpoint, with PKCE (`S256`) and the account server as the resource. They sign in with a code we email them, see what you're asking for, and choose Allow or Deny.
   
   ```
   https://getmandatorytraining.com/oauth/authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_uri>&code_challenge=<code_challenge>&code_challenge_method=S256&scope=<scopes>&state=<state>&resource=https://getmandatorytraining.com/mcp/account
   ```
3. Exchange the code for tokens.
   
   ```
   curl -X POST https://getmandatorytraining.com/oauth/token -d grant_type=authorization_code -d code=<code> \
     -d redirect_uri=<redirect_uri> -d client_id=<client_id> -d code_verifier=<code_verifier>
   ```
4. Call the account MCP server with the access token.
   
   ```
   curl -X POST https://getmandatorytraining.com/mcp/account -H 'Authorization: Bearer <access token>' \
     -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
     -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
   ```
5. Revoke your tokens when you're done (the learner can also disconnect you from their account page).
   
   ```
   curl -X POST https://getmandatorytraining.com/oauth/revoke -d token=<refresh token> -d client_id=<client_id>
   ```

Scopes:

- `account.read`: See your name, email, country and your courses and progress.
- `certificates.read`: See and download your certificates.

Access tokens last 60 minutes, and refresh tokens rotate on every use. A connection lasts 30 days from the learner's consent. The full description is in [auth.md](https://getmandatorytraining.com/auth.md), with the [authorization server metadata](https://getmandatorytraining.com/.well-known/oauth-authorization-server) and the [protected resource metadata](https://getmandatorytraining.com/.well-known/oauth-protected-resource).

### Access and limits

Everything here is free: there's no paid tier, no sign-up and no API key. Reads change nothing, so call the live API for them. The two calls that do something, registering for a learner and enrolling, have a test mode; see [Sandbox](https://getmandatorytraining.com#sandbox). Enrolling and allowing account access are always done by the person themselves, and connecting for a learner is self-serve, with no approval from us. Reads are limited per client address, to 120 requests every 60 seconds: each read response says where you stand in its `RateLimit-Policy` and `RateLimit` headers, and over the limit a 429 carries `Retry-After` (see [API conventions](https://getmandatorytraining.com#conventions)). Client registration and token requests are throttled per address.

## Endpoints

### `GET /agent/courses`

The courses for a country, with prices in that country's currency. `country` is a two-letter code (`US`, `CA`, `GB`, `NZ` and others); without it, the answer is for the requester's own country as we estimate it.

```
curl 'https://getmandatorytraining.com/agent/courses?country=NZ'
```

### `GET /agent/courses/{slug}`

One course: its description, lesson titles, the size and pass mark of its test, and its versions for other countries. An unknown slug answers 404 with a `not_found` problem (see [Errors](https://getmandatorytraining.com#errors)).

```
curl 'https://getmandatorytraining.com/agent/courses/first-aid-nz'
```

### `GET /agent/acceptance`

Our employer-acceptance wording for a country on its own, with the same `country` parameter.

```
curl 'https://getmandatorytraining.com/agent/acceptance?country=GB'
```

## Enrolling

Enrolment happens on the site, not through this API. Send the person to the course's `url`: they enter and confirm their own details there, and we email them a sign-in link that starts the course.

## Sandbox (test mode)

To rehearse the calls that send an email, use a reserved test address: one at `example.com`, `example.net` or `example.org` exactly, or at any domain ending `.test`. These can never belong to a person. A test request is checked exactly like a real one and counts against the same limits, but nothing is sent and nothing is stored: no account, enrolment, app or connection. Its answer is the usual one plus `"test": true`. [The sandbox page](https://getmandatorytraining.com/sandbox) has the full rehearsal, step by step.

### Registering for a learner

`POST https://getmandatorytraining.com/agent/auth` with a test email answers as usual. Poll the token endpoint with its `claim_token`: the first poll answers `authorization_pending`, and the next, after `interval` seconds, returns test tokens with no person involved.

```
curl -X POST https://getmandatorytraining.com/agent/auth -H 'Content-Type: application/json' \
  -d '{"name":"<your app>","email":"agent@example.com","scope":"account.read certificates.read"}'
curl -X POST https://getmandatorytraining.com/oauth/token -d grant_type=urn:workos:agent-auth:grant-type:claim \
  -d client_id=<client_id> -d claim_token=<claim_token>   # authorization_pending
sleep 5
curl -X POST https://getmandatorytraining.com/oauth/token -d grant_type=urn:workos:agent-auth:grant-type:claim \
  -d client_id=<client_id> -d claim_token=<claim_token>   # test access and refresh tokens
```

Test tokens start `gmt_test_`, last as long as real ones, and refresh and revoke the same way. On the account MCP server they only ever read a made-up **sample learner**: two sample courses and one sample certificate, every result marked `"sample": true`. A test token can never read a real account, and a real token never sees the sample.

### Enrolling

The enrolment call behind our in-page tools takes a test address too. Get a form token first, and wait at least `min_wait_ms` before using it:

```
curl https://getmandatorytraining.com/agent/enrol-token
curl -X POST https://getmandatorytraining.com/agent/courses/cpr-aed/enrol -H 'Origin: https://getmandatorytraining.com' \
  -d name='Test Learner' -d email=learner@example.org -d country=US -d state_us=NY -d ft=<token>
```

It answers `{"result":"email_sent","test":true,…}`, and no email is sent.

## MCP

The same three reads are an MCP server, for assistants that connect to one (as a custom connector, for example). It needs no sign-in and changes nothing: `list_courses`, `get_course` and `employer_acceptance`, each with an optional `country`.

```
https://getmandatorytraining.com/mcp
```

Enrolment stays on the site: send the person to the course's `url`.

### The docs MCP server

A third MCP server, `https://getmandatorytraining.com/mcp/docs`, serves this page and the site's other developer and trust documents (the developer portal, auth.md, the developer llms.txt, the OpenAPI description, about, privacy and terms) as resources, with `search_docs` and `get_doc`. Every document is the site's own published text. It needs no sign-in and changes nothing.

### The account MCP server

A second MCP server, `https://getmandatorytraining.com/mcp/account`, reads one learner's own account, read-only, with a token the learner granted (see [Authentication](https://getmandatorytraining.com#authentication)). It can never take lessons or tests, get or pay for certificates, or change anything.

## A2A

The same three reads are an Agent2Agent (A2A) agent, for other agents: JSON-RPC at `https://getmandatorytraining.com/a2a`, described by its agent card at [`/.well-known/agent-card.json`](https://getmandatorytraining.com/.well-known/agent-card.json). It answers A2A 1.0 (`SendMessage`) and 0.3 (`message/send`). Send a data part naming the skill and its arguments; free text isn't interpreted, and gets a reply explaining how to call it. The reply is a completed task whose artifact holds the same JSON the REST API returns, and a short text summary. It stores no tasks, and doesn't stream.

```
curl -X POST https://getmandatorytraining.com/a2a -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"1","role":"ROLE_USER","parts":[{"data":{"skill":"list_courses","country":"NZ"}}]}}}'
```

## NLWeb /ask

`https://getmandatorytraining.com/ask` answers a natural-language question in Microsoft's NLWeb protocol (0.5, and the earlier REST form), by retrieval only: it ranks the courses and these documents by the question's words and returns the site's own data unchanged. It writes no answer of its own. Each result has `url`, `name`, `site`, `score` (0 to 1, relative to the best) and `schema_object`: a course's schema.org `Course` with its certificate `Offer`, exactly as its page carries it, or a documentation section. The reply also carries `_meta` (`response_type` `Answer`, `version` `0.5`) and 0.5's `content`.

```
curl -X POST https://getmandatorytraining.com/ask -H 'Content-Type: application/json' -d '{"query":"first aid New Zealand"}'
```

To stream the results as server-sent events (`start`, one `result` each, then `complete`), ask for it:

```
curl -N -X POST https://getmandatorytraining.com/ask -H 'Content-Type: application/json' -d '{"query":"CPR","prefer":{"streaming":true}}'
```

`GET https://getmandatorytraining.com/ask?query=…` works too (add `&streaming=true` to stream). A request without a query answers 400. `prev` (or 0.5's `context.prev`) is accepted but doesn't change the ranking, since nothing rewrites your query: for a follow-up, send the whole question as `decontextualized_query`.

## Agents in the browser

On our pages, an agent in the person's browser can use our WebMCP tools instead: `list_courses`, `get_course`, `employer_acceptance`, `set_country` and `start_enrolment`, which asks the person to confirm before it sends anything.

## Errors

Every error from `/agent/`, and a missing page asked for as JSON, is an RFC 9457 problem served as `application/problem+json`: `type` (a link here), `title`, `status`, `code`, `detail` and `hint`, which says what to do next. Errors from the read API also keep `result`, the same code, for clients written before problem details.

- `not_found`: there's nothing at that address, or no course with that slug is offered.
- `invalid`: the request is missing something or has a value we can't use.
- `forbidden`: the call is only accepted from the site's own pages.
- `unavailable`: the catalogue is loading or can't be read; try again shortly.
- `too_many` (429): too many requests of this kind for now; wait the seconds its `Retry-After` header gives before trying again.
- `method_not_allowed` (405): the resource exists but not with that method; the `Allow` header lists the ones it accepts (`GET, HEAD` on the read API).

The OpenAPI description documents every error each operation can answer, and a `default`, as this same Problem schema, so a client generated from it has a type for every error.

```
{"type":"https://getmandatorytraining.com/agent/docs#errors","title":"Not found","status":404,"code":"not_found","detail":"No course with that slug is offered.","hint":"List the courses offered with GET https://getmandatorytraining.com/agent/courses, and use a slug from that list.","result":"not_found"}
```

## API conventions

Conventions: this is version 1.0; a breaking change comes with a new version, and additive changes don't. Reads are limited per client address; every read response carries RateLimit-Policy and RateLimit (draft-ietf-httpapi-ratelimit-headers), and a 429 carries Retry-After. Pagination, batch requests, async jobs and idempotency keys don't apply: every documented operation is a read-only GET returning a complete, short list or one item.

## Machine-readable

The [OpenAPI description](https://getmandatorytraining.com/agent/openapi.json) (version 1.0, also at `/openapi.json`, `/swagger.json`, `/api/openapi.json` and `/.well-known/openapi.json`) and the [API catalog](https://getmandatorytraining.com/.well-known/api-catalog). Every public page links to both in its `Link` header.

Mandatory Training is an independent training provider, not affiliated with or endorsed by the American Heart Association®, the Red Cross®, the Heart and Stroke Foundation of Canada, the Canadian Red Cross, Resuscitation Council UK, the British Red Cross, St John Ambulance, the New Zealand Resuscitation Council, ANZCOR, St John New Zealand, New Zealand Red Cross or any government body. Course content is based on published AHA/ECC and ILCOR guidelines. Fully online training does not include a hands-on skills assessment, and some employers and licensing bodies require one, so check with yours before you rely on it.
