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, courseDetails, hasEhcp.
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).
gender is a string, not a number, and it must match one of the supported values exactly, including capitalisation and punctuation: Male, Female, NotDisclosed, Non-binary/Non-conforming, or Another. An unrecognised value is rejected with a 400 naming LearnerBaseDetails.Gender, and so is a numeric value. The reads return the same strings.
courseDetails is free text describing what the learner is studying, capped at 230 characters; a longer value is rejected with a 400. hasEhcp records whether the learner has an Education, Health and Care Plan and defaults to false when you omit it. Both are also returned on the reads and can be changed later with PUT /v1/learners/{learnerId}.
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. Treat it as a replace, not a patch: build the request body from the learner's current full record, change the fields you mean to change, and send the whole thing. Read the learner first with GET /v1/learners/{learnerId} if you do not already hold their current values.
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 '{
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com",
"gender": "Female",
"postcode": "NE1 6BF",
"dateOfBirth": "2000-02-24",
"clientReference": "12345ABCDE",
"primaryTutorEmail": "new.tutor@example.com",
"endDate": "2030-06-30"
}'
firstName, lastName, email, gender, dateOfBirth and primaryTutorEmail are effectively required on update whatever the schema marks as optional: omitting any of them fails validation with a 400.
courseDetails and hasEhcp are safe to omit
These two fields, and only these two, carry an explicit omit-to-keep guarantee: leave them out of the body and the stored values are preserved rather than cleared. That is what lets an integration written before they existed keep calling PUT without wiping them. To clear courseDetails deliberately, send it explicitly as null or ""; omitting it will not clear it. hasEhcp takes only true or false and returns a 400 if you send null. See what courseDetails and hasEhcp mean.
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 thelearnerIdsin 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. OmitreplacementTutorEmailto 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: Location as a first-class filterable learner field is on the roadmap.
Tags¶
Tags are your organisation's own labels for learners, such as a course code or a cohort name. A tag carries no meaning to Cognassist: it exists to group learners the way your organisation already thinks about them, and it is separate from courseDetails, which describes what a learner is studying and feeds assessment personalisation.
Tag a learner¶
Reach for this when you want your own grouping to travel with the learner: add the tag once the learner exists - a separate call after the create, or as part of your regular sync - and it is there for your staff in the app.
- Add tags with
POST /v1/learners/{learnerId}/tags, sending{"tags": ["Year-2", "cohort:2026"]}. Adding is additive: tags the learner already holds are left alone, and any tag your organisation does not have yet is created for you, which is how a tag comes into existence over the API. - Remove a tag with
DELETE /v1/learners/{learnerId}/tags?name=Year-2. This detaches the tag from that one learner, leaving your vocabulary and every other learner untouched.
Three behaviours change how you call these:
- Names match case-insensitively, and the first casing wins.
Closedandclosedare the same tag, and whichever casing created it is the one that displays from then on. Sending a different casing later does not rename it. - Both operations are safe to repeat. Adding a tag the learner already holds, or removing one they do not, succeeds and changes nothing.
- You cannot replace a tag or clear a learner's tags in one call. An empty list is deliberately a no-op rather than a delete, so remove a tag and add its replacement as two calls.
A learner may hold at most 10 tags, and a name must be 3 to 20 characters of letters, digits, : or -, with no spaces. Break either rule when adding and you get a 400 naming the problem, with nothing applied. Removal is not validated the same way, deliberately: it is idempotent, so a name that breaks those rules is simply a name the learner does not hold, and you get 204 rather than a 400. An unknown learner returns 404, as does a learner belonging to another organisation.
Manage your vocabulary¶
Two operations act on your tag vocabulary rather than on a single learner:
- List your tags with
GET /v1/tags. It returns every live tag in your organisation as anidand aname, in the casing the tag was created with, sorted by name. There is no paging and no per-tag learner count: this is the vocabulary, not the assignments. - Delete a tag with
DELETE /v1/tags?name=Year-2. The name is matched case-insensitively, and it is sent as a query parameter rather than a path segment because a tag name may contain characters that are unreliable in a path. You get204 No Contenton success, a400if you send no name, and a404if no live tag has that name.
For the exact request and response shapes, see the tags endpoints in the API reference.
Deleting a tag from your vocabulary affects every learner holding it, and cannot be undone
DELETE /v1/tags is organisation-wide, and there is no restore: the tag is removed from every learner that holds it. To detach a tag from a single learner, use DELETE /v1/learners/{learnerId}/tags instead. Re-creating the same name afterwards produces a new tag with a new id, so anything you stored against the old id will no longer match it.
Filtering or grouping learners by tag is not on the API yet. You can read your vocabulary, and tags travel with the learners you set them on, but there is no way to ask for every learner tagged Year-2. That is on the roadmap.
Tagging is switched on per organisation. Until it is switched on for yours, a well-formed, authorised call to any tag route returns 404: ask your Cognassist contact to enable it. The 404 is not a blanket answer for a disabled organisation - a request that is malformed or unauthorised is answered by those checks first, so you may see a 400 or a 403 instead. Treat 404 as "off or absent" and do not read "enabled" into any other status.
Related¶
- Manage staff accounts: provision tutors and other staff, which must exist before you create learners against them.
- Getting started: authenticate and make your first create call.
- Core concepts: join keys and the data model in depth.
- Send the assessment: invite the learner or embed the assessment.
- Keep in sync: webhooks first, polling as the fallback, once a learner is enrolled.
- Power Automate worked example: the reconcile loop as a low-code flow.
- Troubleshooting: diagnosing a rejected create, including the tutor dependency.
- Full request and response schemas: the API reference.