{
  "openapi": "3.1.0",
  "info": {
    "title": "Get Mandatory Training course catalogue API",
    "version": "1.0",
    "summary": "Read-only course information for agents helping a person choose an online CPR or first aid course.",
    "description": "Lists the online courses offered for a country, with prices, what each course covers, and what we say about whether an employer will accept the certificate. Give people the employer_acceptance wording as it is, without paraphrasing it. Enrolment happens on the site, where the person enters and confirms their own details. Documentation: /agent/docs. It's free: no paid tier, no key and no sign-up. 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."
  },
  "servers": [{ "url": "https://getmandatorytraining.com" }],
  "security": [],
  "externalDocs": {
    "url": "https://getmandatorytraining.com/sandbox",
    "description": "No operation needs authentication: reads change nothing, so call the live API. The calls with side effects (agent registration and enrolment) have a test mode; the sandbox page shows how to rehearse them."
  },
  "paths": {
    "/agent/courses": {
      "get": {
        "operationId": "listCourses",
        "summary": "List the courses for a country",
        "description": "The courses offered for the country, one version per course, with that country's employer-acceptance wording. A country without its own versions gets the US versions and a note that says so.",
        "parameters": [{ "$ref": "#/components/parameters/country" }],
        "responses": {
          "200": {
            "description": "The courses for the country.",
            "headers": { "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }, "RateLimit": { "$ref": "#/components/headers/RateLimit" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CourseList" } } }
          },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooMany" },
          "500": { "$ref": "#/components/responses/Failed" },
          "503": { "$ref": "#/components/responses/Unavailable" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/agent/courses/{slug}": {
      "get": {
        "operationId": "getCourse",
        "summary": "Get one course",
        "description": "One course, with its description, its lesson titles, the size and pass mark of its test, and its versions for other countries.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The course's slug, from a course list.",
            "schema": { "type": "string", "pattern": "^[a-z0-9-]+$" },
            "example": "first-aid-nz"
          }
        ],
        "responses": {
          "200": {
            "description": "The course.",
            "headers": { "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }, "RateLimit": { "$ref": "#/components/headers/RateLimit" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Course" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooMany" },
          "500": { "$ref": "#/components/responses/Failed" },
          "503": { "$ref": "#/components/responses/Unavailable" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/agent/acceptance": {
      "get": {
        "operationId": "employerAcceptance",
        "summary": "What we say about employer acceptance",
        "description": "Whether an employer will accept the certificate is up to the employer. This is our wording on it for the country, to give the person as it is.",
        "parameters": [{ "$ref": "#/components/parameters/country" }],
        "responses": {
          "200": {
            "description": "The wording for the country.",
            "headers": { "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }, "RateLimit": { "$ref": "#/components/headers/RateLimit" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Acceptance" } } }
          },
          "405": { "$ref": "#/components/responses/MethodNotAllowed" },
          "429": { "$ref": "#/components/responses/TooMany" },
          "500": { "$ref": "#/components/responses/Failed" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "country": {
        "name": "country",
        "in": "query",
        "required": false,
        "description": "A two-letter country code (US, CA, GB, NZ and others; UK is accepted for GB). Without it, or with a code we don't recognise, the answer is for the requester's own country as the site estimates it, else the United States. Asking for a country never changes anyone's settings.",
        "schema": { "type": "string", "pattern": "^[A-Za-z]{2}$" },
        "example": "NZ"
      }
    },
    "responses": {
      "NotFound": {
        "description": "No course with this slug is open to visitors.",
        "headers": { "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }, "RateLimit": { "$ref": "#/components/headers/RateLimit" } },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "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" } } }
      },
      "Failed": {
        "description": "Something went wrong reading the catalogue. Try again shortly.",
        "headers": { "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }, "RateLimit": { "$ref": "#/components/headers/RateLimit" } },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://getmandatorytraining.com/agent/docs#errors", "title": "Temporarily unavailable", "status": 500, "code": "unavailable", "detail": "The course catalogue isn't available right now.", "hint": "Try again in a minute. If it keeps failing, see https://getmandatorytraining.com/agent/docs.", "result": "unavailable" } } }
      },
      "Unavailable": {
        "description": "The course catalogue is loading or can't be read. Try again shortly.",
        "headers": { "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }, "RateLimit": { "$ref": "#/components/headers/RateLimit" } },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://getmandatorytraining.com/agent/docs#errors", "title": "Temporarily unavailable", "status": 503, "code": "unavailable", "detail": "The course catalogue isn't loaded yet.", "hint": "Try again in a minute. If it keeps failing, see https://getmandatorytraining.com/agent/docs.", "result": "unavailable" } } }
      },
      "MethodNotAllowed": {
        "description": "This path doesn't accept the method used: the read API's paths accept GET and HEAD only. The Allow header names the methods to use. A wrong method isn't a read, so it isn't counted against the reads quota and carries no RateLimit fields.",
        "headers": {
          "Allow": { "description": "The methods this path accepts, comma-separated.", "schema": { "type": "string" }, "example": "GET, HEAD" }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://getmandatorytraining.com/agent/docs#errors", "title": "Method not allowed", "status": 405, "code": "method_not_allowed", "detail": "This resource doesn't accept that method.", "hint": "Use GET or HEAD; see https://getmandatorytraining.com/agent/docs." } } }
      },
      "Error": {
        "description": "Any other error, as a Problem: read code and hint to decide what to do next. Every error the API sends is a Problem.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://getmandatorytraining.com/agent/docs#errors", "title": "Temporarily unavailable", "status": 500, "code": "unavailable", "detail": "The course catalogue isn't available right now.", "hint": "Try again in a minute. If it keeps failing, see https://getmandatorytraining.com/agent/docs." } } }
      },
      "TooMany": {
        "description": "Too many reads from this address in the current window. Wait the Retry-After seconds, which is when the window ends, then try again.",
        "headers": {
          "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" },
          "RateLimit": { "$ref": "#/components/headers/RateLimit" },
          "Retry-After": { "description": "Seconds until the window ends and reads are accepted again: the RateLimit field's t.", "schema": { "type": "integer", "minimum": 1 }, "example": 43 }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" }, "example": { "type": "https://getmandatorytraining.com/agent/docs#errors", "title": "Too many requests", "status": 429, "code": "too_many", "detail": "Too many reads from this address for now.", "hint": "Wait before trying again; the limits are described in https://getmandatorytraining.com/agent/docs.", "result": "too_many" } } }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "The reads quota per client address, as a structured-field list (draft-ietf-httpapi-ratelimit-headers): the policy name \"reads\", q requests per w seconds, counted on each server. For example \"reads\";q=120;w=60.",
        "schema": { "type": "string" },
        "example": "\"reads\";q=120;w=60"
      },
      "RateLimit": {
        "description": "What's left of the reads quota for this address, as a structured-field list (draft-ietf-httpapi-ratelimit-headers): r requests remaining and t seconds until the window ends. For example \"reads\";r=119;t=43.",
        "schema": { "type": "string" },
        "example": "\"reads\";r=119;t=43"
      }
    },
    "schemas": {
      "CourseList": {
        "type": "object",
        "description": "The courses offered for one country, with that country's employer-acceptance wording.",
        "required": ["country", "employer_acceptance", "courses"],
        "additionalProperties": false,
        "properties": {
          "country": { "type": "string", "description": "The country answered for, by name." },
          "note": { "type": "string", "description": "When some or all of the courses are US versions: says they follow US guidelines and refer to US emergency numbers and laws." },
          "employer_acceptance": { "$ref": "#/components/schemas/AcceptanceText" },
          "courses": { "type": "array", "items": { "$ref": "#/components/schemas/Course" }, "description": "The courses, one version each." }
        }
      },
      "Course": {
        "type": "object",
        "description": "One course, as offered in a country. Get a course adds its description, lessons, test and other versions.",
        "required": ["slug", "title", "url", "country", "price", "currency", "minutes", "certificate_valid_for", "summary", "audience", "enrolment", "employer_acceptance"],
        "additionalProperties": false,
        "properties": {
          "slug": { "type": "string", "description": "The course's identifier, for Get a course." },
          "title": { "type": "string", "description": "The course's name." },
          "url": { "type": "string", "description": "The course's page, where the person can enrol." },
          "country": { "type": "string", "description": "The country this version of the course is written for." },
          "price": { "type": "string", "description": "The certificate's price in the course's own currency, e.g. \"NZ$19.95\"." },
          "currency": { "type": "string", "description": "ISO 4217 code." },
          "minutes": { "type": "integer", "description": "About how long the course takes." },
          "certificate_valid_for": { "type": "string", "description": "How long the certificate of completion is valid for, e.g. \"2 years\"." },
          "summary": { "type": "string", "description": "What the course covers, in a sentence or two." },
          "audience": { "type": "string", "description": "Who the course is written for." },
          "enrolment": { "type": "string", "description": "How enrolling works." },
          "employer_acceptance": { "$ref": "#/components/schemas/AcceptanceText" },
          "description": { "type": "string", "description": "Get a course only." },
          "lessons": { "type": "array", "items": { "type": "string" }, "description": "Get a course only: the lesson titles." },
          "test_questions": { "type": "integer", "description": "Get a course only: how many questions the test has." },
          "test_pass_mark": { "type": "integer", "description": "Get a course only: how many correct answers pass." },
          "other_versions": { "type": "array", "items": { "$ref": "#/components/schemas/Version" }, "description": "Get a course only: the same course written for other countries." }
        }
      },
      "Version": {
        "type": "object",
        "description": "The same course written for another country.",
        "required": ["country", "slug", "url"],
        "additionalProperties": false,
        "properties": {
          "country": { "type": "string", "description": "The country this version is written for, by name." },
          "slug": { "type": "string", "description": "This version's identifier, for Get a course." },
          "url": { "type": "string", "description": "This version's page." }
        }
      },
      "Acceptance": {
        "type": "object",
        "description": "Our wording on employer acceptance for one country.",
        "required": ["country", "employer_acceptance"],
        "additionalProperties": false,
        "properties": {
          "country": { "type": "string", "description": "The country answered for, by name." },
          "employer_acceptance": { "$ref": "#/components/schemas/AcceptanceText" }
        }
      },
      "AcceptanceText": {
        "type": "array",
        "items": { "type": "string" },
        "description": "Paragraphs on whether an employer will accept the certificate. Give them to the person as they are, without paraphrasing or shortening them into a yes."
      },
      "Problem": {
        "type": "object",
        "description": "An error, as RFC 9457 problem details, with a stable code and a hint saying what to do next.",
        "required": ["type", "title", "status", "code", "detail", "hint"],
        "additionalProperties": false,
        "properties": {
          "type": { "type": "string", "description": "A link to the errors section of the API docs.", "example": "https://getmandatorytraining.com/agent/docs#errors" },
          "title": { "type": "string", "description": "A short, fixed summary of the kind of error.", "example": "Not found" },
          "status": { "type": "integer", "minimum": 400, "maximum": 599, "description": "The HTTP status code, repeated.", "example": 404 },
          "code": { "type": "string", "enum": ["not_found", "invalid", "forbidden", "unavailable", "too_many", "method_not_allowed"], "description": "A stable, machine-readable error code.", "example": "not_found" },
          "detail": { "type": "string", "description": "What went wrong with this request, in fixed text.", "example": "No course with that slug is offered." },
          "hint": { "type": "string", "description": "What to do next, with a link to the docs or a resource.", "example": "List the courses offered with GET https://getmandatorytraining.com/agent/courses, and use a slug from that list." },
          "result": { "type": "string", "enum": ["not_found", "unavailable", "too_many"], "description": "The same code, kept for clients written before problem details.", "example": "not_found" }
        }
      }
    }
  }
}
