Skip to content

Create and manage learners

A learner is the core record in Cognassist: a person you enrol so they can take the cognitive assessment and receive support tailored to how they think and learn. This guide covers creating learners, keeping them reconciled with your own system of record, and the tutor-assignment model that decides who can see each learner.

All calls use the base URL https://api.uk.cognassist.com and a bearer token (see Getting started). Your credentials are scoped to your organisation, so every endpoint here only ever returns your organisation's learners.

The model in one paragraph

Store one stable id per learner on your side (clientReference) and create each learner once. A learner's email is unique within your organisation, so a second create for the same person is rejected, not duplicated; you still reconcile before creating so you handle that cleanly and keep your own id mapping. When you reconcile, page the learner list and match rows on email, the human key the list returns, and fetch a full record only when you need to confirm or repair the id you stored. Everything below is the detail on top of that.

For the join-key model (which identifier does which job, and why you match on email), see Core concepts. This guide uses those keys; it does not re-explain them.

Create a learner

POST /v1/learners creates a single learner and returns 201 Created with two Cognassist-side GUIDs, learnerId and learnerUserId. Store learnerId against your own record. It is the id you pass on every later call for that learner (invite, retrieve results, read evidence).

curl -X POST "https://api.uk.cognassist.com/v1/learners" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane.doe@example.com",
    "clientReference": "12345ABCDE",
    "primaryTutorEmail": "tutor.name@example.com"
  }'

The example above is trimmed to the fields that carry the model. The create body has more required fields than that, plus several optional ones.

Full create body: required and optional fields

Required: firstName, lastName, email, primaryTutorEmail, gender, postcode, dateOfBirth.

Strongly recommended: clientReference, your own id for the learner and your durable join key. Always send it; without it you have no stable key of your own to reconcile against. (The API reference lists it among the required create fields.)

Optional: programmeLevel (an integer 1 to 7), learnerUniqueReference, mobileNumber, endDate.

programmeLevel is optional, even though Getting started may show a leaner body that omits it. learnerUniqueReference is the optional slot for a ULN: it is validated and must be unique within your organisation when supplied, but it is optional, so it is not your primary join (see Core concepts).

For exact types, formats, and validation rules, see the create endpoint in the API reference.

Two things about create trip integrations if you miss them, so internalise both before you write the loop.

Create does not update; an unresolved primary or secondary tutor rejects the whole request

POST /v1/learners creates; it does not update an existing learner. Both tutor references must resolve to a tutor who already exists: if primaryTutorEmail is missing or does not resolve, the create is rejected with a 400 (Invalid primary tutor.) and no learner is created; likewise, if any email in secondaryTutorEmails does not resolve to a known tutor, the entire request is rejected (400). Provision the tutors first.

A learner's email is unique within your organisation, so calling create again for the same person is rejected (User is already a Learner), not silently duplicated. Still look the learner up before every create and funnel all enrolment through a single check keyed on your clientReference, so two source systems handle an existing learner cleanly rather than tripping that error.

Provision your tutors up front, either with POST /v1/staff (see managing staff accounts) or in the Cognassist app, then map each learner to a tutor email your source system already knows. A learner's primaryTutorEmail must resolve to a tutor who already exists at the time of the create call, so the provisioning has to come first; otherwise the create is rejected with a 400 and no learner is created. Once the tutor account exists, retry the create, or use PUT /v1/learners/{learnerId} to move an existing learner to a different tutor. See Troubleshooting for diagnosing tutor assignment issues.

Batch SFTP as a limited fallback for getting learners in

To get learners in from a file-only source, a one-way-in SFTP upload is a limited fallback. Prefer POST /v1/learners wherever the source system can make an HTTPS call, so you get the learnerId back to react to and the reconcile step to avoid duplicates.

Reconcile against your system

Reconciling means: pull the learners Cognassist already holds, match them against your own records, then create only the ones that are missing (and PUT any that changed). The load-bearing detail is which field you match on, and that is decided by what the list returns.

GET /v1/learners returns a paged list with a top-level totalRecords. The list carries only email as a shared human key: it does not return clientReference and offers no server-side filter on it, so the reconcile loop matches on email, not clientReference. For the full list projection and why the match key is email, see Core concepts. clientReference stays your stable primary key on your own side; the point here is only that the API's list cannot be filtered or matched by it today.

When you have matched on email and need to confirm or repair the stored clientReference (an email changed, or you are backfilling a join you never captured), fetch the full record: GET /v1/learners/{learnerId}, or GET /v1/learners/{email} for a known email (which returns 404 on no match). Both include clientReference and learnerUniqueReference. Reserve this per-learner fetch for the rows that actually need it; running it for every row is roughly one call per learner, which is exactly the tight polling to avoid against an unquantified rate limit.

The loop reads totalRecords, pages the list, matches each row on email, then branches: PUT a changed match or POST a missing learner, stopping once it has collected totalRecords.

flowchart TD
  A[Read totalRecords<br/>from the first page] --> B[Page GET /v1/learners]
  B --> C[Match each row on email]
  C --> D{Email in your<br/>source records?}
  D -- "Found" --> E[PUT only if the record changed]
  D -- "Not found" --> F[POST to create the learner]
  E --> G{Collected<br/>totalRecords?}
  F --> G
  G -- "No" --> B
  G -- "Yes" --> H[Done]
Paging and sizing the run

The list is paginated with pageSize, pageIndex, and includeOffProgrammeLearners. Because the response includes totalRecords, the stop condition is knowable without guessing the page count: stop once you have collected totalRecords items, or when a page returns fewer than pageSize rows.

The spec does not state whether pageIndex starts at 0 or 1, so you do not need that value to write the loop: start from your first page, keep requesting until you have collected totalRecords items (or a page returns fewer than pageSize rows), and the stop condition holds on either base. Confirm the base index in the API reference if you want to log or assert absolute page numbers. totalRecords also lets you size the run up front: at a page size of N, expect about totalRecords / N list calls, far fewer than a per-row fetch.

The Power Automate worked example and Keep in sync both build on this loop; it is the canonical picture, so they link here rather than redraw it.

Update and delete

PUT /v1/learners/{learnerId} returns 204 No Content. Every field is optional: omit a field to leave it unchanged, supply it to replace the value. Send only what changed, such as a new tutor or a corrected reference.

curl -X PUT "https://api.uk.cognassist.com/v1/learners/00000000-0000-0000-0000-000000000000" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "primaryTutorEmail": "new.tutor@example.com", "endDate": "2030-06-30" }'

DELETE /v1/learners/{learnerId} also returns 204. Reserve it for records created in error; use PUT to move a learner between tutors or programmes.

Tutor assignment governs visibility

Assignment is not just an attribute; it decides who in your organisation can see a learner and their results:

  • Each learner has one primary tutor plus any number of secondary tutors.
  • A tutor sees the learners assigned to them; a tutor manager sees their tutors' learners; an admin sees all learners. Leadership dashboards (Insights) are admin-only.
  • Assigning a tutor requires a manager or admin.
  • Primary versus secondary is a professional convention (who owns the plan and records evidence), not an app-enforced read-only lock.

Set the primary tutor at creation via primaryTutorEmail, or change it later with PUT. The tutor must already exist in Cognassist (see the create warning above).

Set secondary tutors on create with secondaryTutorEmails, or replace the whole secondary set for one learner with PUT /v1/learners/{learnerId}/secondarytutors.

To manage one tutor across many learners in a single call, use the tutor-centric batch endpoints. They are keyed by the tutor's email and act on a tutor who already exists: they assign or remove a secondary-tutor relationship, they do not create the tutor account, so provision the tutor first with POST /v1/staff or in the app, as above.

  • Assign a tutor as a secondary tutor across a batch with PUT /v1/tutors/{tutorEmail}/secondarytutor/learners, passing the learnerIds in the body. Re-assigning a tutor who is already a secondary tutor of a learner is idempotent, so the call is safe to retry.
  • Replace or remove a tutor across a batch with PATCH /v1/tutors/{tutorEmail}/secondarytutor/learners. Omit replacementTutorEmail to remove the tutor; supply one to swap it in, which happens only for learners who already have that tutor as a secondary tutor.

Both return 204 No Content, cap a single request at 50 learners, and are all-or-nothing: if any learner id is unknown, a learner is not eligible for a secondary-tutor change, or the tutor or replacement is already a batched learner's primary tutor, the whole request is rejected with a 400 and nothing is applied. Both require a manager or admin, the same as assigning a tutor above. For the full request and response shape, see the tutor endpoints in the API reference.

Planned: Tags and Location as first-class filterable learner fields are on the roadmap.