Skip to content

Troubleshooting

Most first errors on this API are not bugs. They are one of a handful of well-understood cases: a tutor who does not exist yet, a learner who has already finished, or a result you asked for before a real person had completed it.

Find your symptom in the index below, open its entry for the cause and fix, and follow the link to the guide that covers it in full. Each entry is collapsed so the page stays short. Open only the ones you need.

Symptom index

Symptom Likely cause Fix
Create succeeds, but learner has no primary tutor or wrong tutor assigned Unknown primaryTutorEmail is silently skipped Primary tutor invalid or missing
A repeat create is rejected, or a person exists twice Email is unique per org, so create rejects a duplicate; a twin can only come from another route Duplicate or rejected create
Create rejected on a field A required field is missing or the wrong type Create rejected on a field
Invite or URL returns 400 The learner has already completed Invite or URL returns 400
Result reads not completed A real person has not finished the assessment yet Result reads not completed
300 or 800 failure webhook An async report or export could not be generated Report or export failure events
401, 403, 404 or 429 An auth, scope, not-found or rate case Auth and status errors
Webhook signature does not match The HMAC was computed over the wrong bytes Webhook signature mismatch
Cannot match your roster on clientReference The paged list does not return that field Cannot match on clientReference

The error codes, statuses and enums referenced below are all defined in the API reference. This page tells you what each one means for your integration and how to resolve it, then links you there for the exact schema.

Learner creation

Primary tutor invalid or missing

Symptom. A learner is created but has no primary tutor assigned, or the tutor visible in Cognassist is not the one you specified.

Cause. primaryTutorEmail must belong to a tutor who already exists in Cognassist. The API resolves the email to a tutor server-side; if the email does not match a known tutor, the learner is still created but with no primary tutor assigned. There is no validation error — the unresolved email is silently skipped. There is no API to list or create tutors, so the missing tutor cannot be created from your integration.

Fix.

  1. Confirm the tutor exists in Cognassist before you create learners against them. If you are not sure, ask your Cognassist contact to provision your tutors first.
  2. Map each learner to a tutor email your source system already holds, and make sure it matches the one set up in Cognassist exactly.
  3. If a learner is created with no tutor, assign the tutor with PUT /v1/learners/{learnerId} once the tutor account is provisioned.

Treat "the tutors exist in Cognassist" as a pre-integration dependency you tick off once, the same way you set up API credentials. After that, primaryTutorEmail on every create just resolves. Note that unknown secondary tutor emails behave differently: they reject the whole create request (400). See Create a learner for the full model, and Get access for how to reach your Cognassist contact.

Duplicate or rejected create

Symptom. Either a create is rejected with User is already a Learner, or the same person appears twice with two different learnerId values.

Cause. A learner's email is unique within your organisation, so POST /v1/learners rejects a second create for someone who is already a learner, and it never updates an existing record. A genuine twin therefore does not come from the API creating blindly; it comes from a second enrolment route, for example the Cognassist app or an SFTP upload running alongside your API integration.

Fix.

  1. Keep your own clientReference unique and stable per person. It is the id your system owns and the key you reconcile on. See Core concepts for the join-key model.
  2. Look the learner up before every create, so you expect and handle the "already a learner" case instead of treating it as a surprise.
  3. Funnel every enrolment route through a single check keyed on your clientReference, so the app, SFTP, and your API integration cannot each create the same person.

The paged list matches on email (unique within your organisation) and does not return clientReference, so the two keys work together: the list's email match finds a record Cognassist already holds, and your stored clientReference is the durable join you reconcile against. See Reconcile against your system.

If you have already created a genuine duplicate in error, remove the surplus record with DELETE /v1/learners/{learnerId}. Reserve delete for records created in error; use PUT to move a real learner between tutors or programmes.

Create rejected on a field

Symptom. POST /v1/learners returns a 400 with a problem-details body naming a field.

Cause. A required field is missing, or a value is the wrong type or shape.

Fix. Check the field the error names against these easy-to-miss cases:

  • programmeLevel is optional. It is an integer from 1 to 7. Omit it entirely if you do not know it rather than sending 0, an empty string, or a guess. If you do send it, keep it inside the 1 to 7 range.
  • primaryTutorEmail must resolve to an existing tutor. See Primary tutor invalid or missing.
  • learnerUniqueReference is optional but validated when you send it: letters and numbers, no spaces, up to the published maximum, and unique within your organisation. It is the slot for a Unique Learner Number, not your primary key. Leave it out if you do not hold one; a duplicate value is rejected.
  • clientReference: always send the stable id your own system owns for this person. It is your join key, so include it on every create; its absence alone will not trip this error.

For the full field list, exact types, and validation rules, use the API reference. The create body in context is covered in Create a learner.

Assessment lifecycle

Invite or URL returns 400

Symptom. POST /v1/learners/{learnerId}/assessment/invite or GET /v1/learners/{learnerId}/assessment/url returns 400 Bad Request.

Cause. There are two distinct 400 cases, and telling them apart is the whole fix:

  1. The learner has already completed an assessment. There is no live invitation to send and no live URL to hand out, because there is nothing left to start. This is the common case.
  2. The invite is inside the three-day window. You cannot send an invitation email to the same learner within three days of a previous one, which protects Cognassist's sending reputation. This applies to the invite email only, not the embedded URL.

Fix.

  • Check completion first. Read the assessment status with GET /v1/learners/{learnerId}/assessment. If it is Completed, there is nothing to invite them to, so read the result instead. See Retrieve and render results.
  • If you hit the three-day window and a learner genuinely needs to start sooner, hand them the embedded URL from GET /v1/learners/{learnerId}/assessment/url instead of the email invite.

Rather than sending an invite and catching the 400, read status first and only invite learners who have not completed. It is one extra call and it makes both 400 cases moot. The invite and URL flows are covered in Send the assessment.

Result reads not completed

Symptom. You created the learner and sent the invite, then read GET /v1/learners/{learnerId}/assessment and the status comes back as Not completed, or the profile fields are empty.

Cause. This is expected, not an error. The assessment is a set of exercises a real person works through. Until a real person completes it, there is no cognitive profile to return. Sending the invite does not complete the assessment; a person completing it does.

Fix.

  1. Wait for the learner to complete. Build for the gap between invite and result rather than expecting a profile on the next call.
  2. Subscribe to the 100 Assessment Completed webhook instead of polling on a timer. It carries the learnerId the moment a learner finishes, so you fetch each result exactly once, when it is ready. Webhooks are the primary way to react to completion; see Webhooks.
  3. Branch on status: Not completed is 20, Awaiting results is 10, and Completed is 30. All three are from the LearnerAssessmentStatus enum in source; there are no "magic numbers" to guess.

Polling this endpoint on a loop still only tells you what 100 Assessment Completed would push to you for free, and it spends calls against an unquantified rate limit. Wire the webhook once and react to it. Polling is the fallback for when you genuinely cannot receive webhooks; see Keep in sync for the trade-off.

Reports, exports and delivery

Report or export failure events

Symptom. You asked for a PDF report or a bulk evidence export, and instead of the file you receive a 300 Assessment Report Request Failed or an 800 Learner Export Failed webhook.

Cause. Both the PDF report (POST /v1/learners/{learnerId}/assessment/report) and the bulk evidence export are generated asynchronously. The request returns a correlation handle straight away, then the finished file arrives later by webhook. If generation does not complete for that request, the failure event is how you find out. It is the intended signal, not a silent drop.

Fix.

  1. Subscribe to the failure events, not just the success ones. Subscribe to 300 Assessment Report Request Failed alongside 200 Assessment Report Created, and to 800 Learner Export Failed alongside 700 Learner Export Completed. If you only listen for success, a failure looks like a file that never arrived.
  2. Correlate on the request handle. The report POST returns a requestId; match the failure event's RequestId back to it so you know which request failed.
  3. Surface it and retry the request. Log the failure against the request, surface it rather than dropping it, and re-issue the original request if appropriate. A transient generation failure usually succeeds on a fresh request.
flowchart TD
    A[Request report or export] --> B[Get a request handle back]
    B --> C{Which webhook arrives}
    C -->|Success 200 or 700| D[Match handle, store the file]
    C -->|Failure 300 or 800| E[Log against the request, surface it]
    E --> F[Re-issue the original request]

The report flow, including the multipart delivery of the file itself, is in Retrieve and render results. The bulk export flow is in Export evidence and data.

Webhook signature mismatch

Symptom. Your endpoint receives a webhook, but the signature you compute does not match the x-cognassist-sha256 header, so you correctly reject it, and now nothing is getting through.

Cause. The HMAC was almost always computed over the wrong bytes. The signature is HMAC-SHA256 over the raw request body, keyed with your webhook Secret. If your framework has already parsed, re-serialised, or trimmed the body before you hash it, the bytes differ and the signature will never match. The usual culprit is a middleware that deserialises JSON to an object and back, or strips or reorders whitespace.

Fix.

  1. Hash the raw body, exactly as received. Read the raw request body once, before any JSON parsing or model binding, and compute HMAC-SHA256 over those exact bytes with your webhook Secret.
  2. For the multipart event, sign the whole body. The 200 Assessment Report Created event is multipart/form-data (a data part plus a file part). The signature is computed over the entire raw body, so buffer the whole request and verify the signature before you read the form parts.
  3. Compare in constant time. Compare your computed value to the header using a constant-time comparison, then reject anything that does not match.

The step-by-step verification, with a copy-and-paste C# example for both the JSON and multipart cases, is in Verify the signature.

If nothing is arriving at all, that is a different problem from a mismatch: a mismatch means deliveries are arriving and being rejected. Check the delivery history with GET /v1/webhooks/{webhookId}/invocationlogs, confirm the subscription exists and points at the right URL, and run a scheduled reconcile against the API to close any gap. The finer delivery guarantees (retry schedule, endpoint timeout, source IP allow-list) are not published, so confirm those with your Cognassist contact. See Keep in sync and the delivery notes in Webhooks.

Auth, status and reconcile

Auth and status errors (401, 403, 404, 429)

Symptom. A call returns 401, 403, 404, or 429.

Cause and fix. These are the standard statuses shared across every endpoint. Each has a distinct meaning and a distinct fix, so do not treat them all as "the call failed":

Status Meaning Fix
401 Unauthorized The bearer token is missing, malformed, or expired. Re-authenticate with POST /v1/auth and retry with the fresh token. Cache the token for its full lifetime; do not re-authenticate per call.
403 Forbidden The token is valid, but the resource is outside your organisation's scope. Expected for a learner belonging to another organisation. Handle it gracefully rather than as a failure, and check you are using the right learnerId.
404 Not Found No resource matches the id or path. Confirm the learnerId or path is one you actually created and stored. GET /v1/learners/{email} returns 404 when there is no learner for that email.
429 Too Many Requests You are sending requests too fast. Back off exponentially and retry, rather than retrying immediately.

There are no published numeric limits and no Retry-After or X-RateLimit-* header, so cache the token, prefer webhooks over tight polling, and back off on 429. The full guidance is in Getting started.

Cannot match on clientReference

Symptom. You page through GET /v1/learners to reconcile your roster, and the rows do not carry clientReference, so you cannot match your records to Cognassist's on the key you expected to use.

Cause. This is expected. The paged list returns, per learner, only learnerId, learnerUserId, firstName, lastName, email, and proactivelyProvidingSupport. It does not return clientReference or learnerUniqueReference, and there is no server-side filter on either. The full-learner responses from GET /v1/learners/{learnerId} and GET /v1/learners/{email} do carry both fields. See the exact list projection in the API reference.

Fix.

  1. Match the paged list on email. Email is the only shared human key the list carries, so your reconcile loop keys on it.
  2. Fetch the full record only when you need the join. 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), pull the full learner with GET /v1/learners/{learnerId} or GET /v1/learners/{email}.
  3. Keep clientReference as your side's primary key. You still own it, keep it unique, and de-duplicate on it. The only limitation is that the API's list cannot be filtered or matched by it today.

Do not fetch the full record for every row: a per-learner GET for every row is roughly one call per learner, so for an estate of thousands that is thousands of extra calls per run against an unquantified rate limit. Use the paged list matched on email for the routine reconcile, and reserve the full GET for the rows where you actually need the join. The complete recipe is in Reconcile against your system.

Still stuck?

If your symptom is not listed, or a fix did not resolve it:

  • Check the exact schema. Field names, types, enum values, and error bodies are all in the API reference. A mismatch there explains most 400 responses.
  • Check whether the capability is live. If you are looking for something that is not yet available, it may be on the Roadmap rather than a bug in your integration.
  • Contact your Cognassist contact for anything that needs a human: provisioning tutors, confirming delivery guarantees or turnaround, and account-level questions. See Get access for the route to reach them.