records

17 endpoints.

POST/api/records/submissions Bearer token

Submit a record and its evidence in one request

Creates a learning record together with its evidence rows, atomically. The record and evidence parts are JSON-encoded (shaped like CreateLearningRecordRequest and an array of CreateSupportingEvidenceRequest); the bytes of at most one file evidence entry travel in the file part, so no separate upload round trip is needed. Requires records:write over the owner — or none at all for a pending submission about yourself, which a manager then approves.

Request body

multipart/form-datarequiredLearningRecordSubmissionForm
FieldTypeRequiredDescription
recordstringyes

JSON-encoded record, in the shape of the CreateLearningRecordRequest schema. Callers without records:write over the owner must use status "pending" and omit learner_user_id (the record is about themselves).

at least 1 character

evidencestringyes

JSON-encoded array of evidence, each entry in the shape of the CreateSupportingEvidenceRequest schema (without pathname — a file travels in the file part instead). At most one entry may be of evidence_type file.

at least 1 character

filestring (binary)no

The bytes of the one file evidence entry, sent as a file part. Required exactly when the evidence array contains a file entry; the part's size and content type must match what that entry's payload declares.

Responses

StatusDescriptionBody
201The created record and its evidence rows.LearningRecordSubmission
400A part failed validation, or the file part does not match the declared file evidence.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller does not hold records:write over the owner, and this is not a pending submission about themselves.ErrorResponse
404The organization, department, learner, or a tagged subject does not exist there.ErrorResponse
409The organization already has a record with that external_id.ErrorResponse
500The file could not be stored, or the write failed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
GET/api/records Bearer token

List learning records

Lists the learning records the caller can read — their records:read scope, their own records, and the pending submissions of learners in their manager chain — newest first, with cursor pagination.

Parameters

NameInTypeRequiredDescription
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
departmentquerystringnoDepartment UUID id or slug, resolved within org (which is then required).
learnerquerystring (uuid)noOnly records about this learner (`users.id`).
subjectquerystringnoOnly records filed under this subject matter: UUID id, or slug resolved within org.
statusquerystringno—
evidence_typequerystringnoOnly records carrying at least one evidence row of this type.
include_archivedquerystringnoInclude archived rows. Defaults to false.
limitqueryintegernoPage size, 1-200. Defaults to 50.
cursorquerystringnoOpaque cursor from a previous page's next_cursor.

Responses

StatusDescriptionBody
200One page of records, and the cursor for the next.LearningRecordList
400A filter did not resolve, or the cursor is malformed.ErrorResponse
401The access token is missing or invalid.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/records Bearer token

Create a learning record

Creates a record owned by an organization or one of its departments, optionally tagged with subjects. Requires records:write over the owner. Omit learner_user_id to record about yourself. hidden_from_learner keeps the record from the learner's own view.

Request body

application/jsonrequiredCreateLearningRecordRequest
FieldTypeRequiredDescription
owner_organization_idstring (uuid)yes

Organization the record belongs to. Always required.

owner_department_idstring (uuid)no

Omit for a record owned by the organization directly.

learner_user_idstring (uuid)no

`users.id` of the learner. Omit to record about yourself — the snapshots below then default to your own profile.

learner_display_namestringno

1–300 characters

learner_emailstringno

1–300 characters

titlestringyes

1–300 characters

descriptionstringno

at most 5000 characters

statusstringno

Callers without records:write over the owner may only create pending records about themselves. rejected cannot be set directly — it is entered through the reject endpoint.

one of "pending" | "in_progress" | "completed" | "expired" | "revoked" · defaults to "completed"

completed_atstring (date-time) | nullno

When the training or assessment happened. Required on a pending submission about yourself: a recertification window counts only records completed after the current award, so an undated one could never count.

expires_atstring (date-time) | nullno

Must be after completed_at when both are set.

external_idstringno

Importer-assigned id; unique per owning organization, so re-runs are idempotent.

1–300 characters

subject_matter_idsarray of string (uuid)no

Subjects to file the record under; must belong to the same organization.

at most 100 items · defaults to []

hidden_from_learnerbooleanno

Hide the record from the learner it is about: they can then read it only through records:read over the owner. Needs records:write over the owner — a self-service submission cannot be hidden — and a status other than pending or rejected.

defaults to false

Responses

StatusDescriptionBody
201The created record.LearningRecord
400The request body failed validation, a pending submission about yourself omitted completed_at, or a hidden record was given a submission status.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller does not hold records:write over the owner.ErrorResponse
404The organization, department, learner, or a tagged subject does not exist there.ErrorResponse
409The organization already has a record with that external_id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/records/export.csv Bearer token

Export learning records as CSV

The learning records the caller can read — exactly what `GET /records` lists, under the same filters — as one CSV file, newest first, one row per record with the columns `id`, `owner_organization_id`, `owner_organization_name`, `owner_department_id`, `owner_department_name`, `learner_user_id`, `learner_display_name`, `learner_email`, `title`, `description`, `status`, `completed_at`, `expires_at`, `external_id`, `recorded_by`, `approved_by`, `approved_at`, `rejected_by`, `rejected_at`, `rejection_note`, `hidden_from_learner`, `subject_ids`, `subject_names`, `evidence_count`, `hours`, `archived_at`, `created_at`. Subjects are `; `-joined lists, timestamps ISO 8601, and the file opens with a UTF-8 byte order mark for spreadsheets. The export carries at most `limit` rows — up to 5000 for most callers, 50000 for superusers — and is refused outright, never truncated, when more records match: narrow the filters or raise the limit.

Parameters

NameInTypeRequiredDescription
orgquerystringnoOrganization UUID id or slug to filter by (and to resolve slug refs in).
departmentquerystringnoDepartment UUID id or slug, resolved within org (which is then required).
learnerquerystring (uuid)noOnly records about this learner (`users.id`).
subjectquerystringnoOnly records filed under this subject matter: UUID id, or slug resolved within org.
statusquerystringno—
evidence_typequerystringnoOnly records carrying at least one evidence row of this type.
include_archivedquerystringnoInclude archived rows. Defaults to false.
limitqueryintegernoThe most rows to export: 1-5000 (superusers: up to 50000). Defaults to the caller's ceiling. When more records match than this, the export is refused rather than truncated.

Responses

StatusDescriptionBody
200The CSV file, served as an attachment named `learning-records-<date>.csv`.—
400A filter did not resolve, or the limit is above the caller's ceiling.ErrorResponse
401The access token is missing or invalid.ErrorResponse
422More records match than the limit allows; nothing is exported.ErrorResponse
500The database could not be read.ErrorResponse
GET/api/records/{record_id}/permanent-deletion Bearer token

Preview permanently deleting a learning record

Counts everything DELETE on this path would remove or unlink: the supporting evidence and its files, the subject tags whose hours stop counting, the imported LMS grade behind the record, and the quiz, SCORM and attendance rows that reported into it. Nothing is changed. Requires records:delete over the record's owner — a grant separate from records:write, which only archives.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Responses

StatusDescriptionBody
200What the deletion would take with it.DeletionImpact
401The access token is missing or invalid.ErrorResponse
403The caller does not hold records:delete over the record's owner.ErrorResponse
404No visible record has that id.ErrorResponse
500The database could not be read.ErrorResponse
DELETE/api/records/{record_id}/permanent-deletion Bearer token

Permanently delete a learning record

Hard-deletes the record — unlike DELETE /records/{record_id}, which only archives it and can be undone. Its supporting evidence goes with it, uploaded files included, and so do its subject tags: the hours and completion it credited toward certification requirements disappear. Quiz, SCORM and event attendance rows that reported into it survive, unlinked. Irreversible. Preview the cost with GET first. Requires records:delete over the record's owner — a grant separate from records:write, which only archives.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Responses

StatusDescriptionBody
204The record and its evidence are gone.—
401The access token is missing or invalid.ErrorResponse
403The caller does not hold records:delete over the record's owner.ErrorResponse
404No visible record has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/records/{record_id} Bearer token

Get a learning record

Returns one record with its subjects. Requires records:read over its owner, being its learner, or — while it is pending — being in its learner's manager chain.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Responses

StatusDescriptionBody
200The record.LearningRecord
401The access token is missing or invalid.ErrorResponse
404No visible record has that id.ErrorResponse
500The database could not be read.ErrorResponse
PATCH/api/records/{record_id} Bearer token

Update a learning record

Changes a record's fields, and replaces its subject set when subject_matter_ids is present. Requires records:write; hidden_from_learner in particular is only ever set by a records:write holder, and never on a pending or rejected record.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Request body

application/jsonrequiredUpdateLearningRecordRequest
FieldTypeRequiredDescription
owner_department_idstring (uuid) | nullno

Move the record to another department of the same organization; null hands it to the organization directly.

learner_display_namestring | nullno

1–300 characters

learner_emailstring | nullno

1–300 characters

titlestringno

1–300 characters

descriptionstring | nullno

at most 5000 characters

statusstringno

rejected cannot be set directly — use the reject endpoint. The learner of a rejected record may set it back to pending, which resubmits it and clears the rejection.

one of "pending" | "in_progress" | "completed" | "expired" | "revoked"

completed_atstring (date-time) | nullno—
expires_atstring (date-time) | nullno—
external_idstring | nullno

1–300 characters

subject_matter_idsarray of string (uuid)no

When present, replaces the record's whole subject set.

at most 100 items

hidden_from_learnerbooleanno

Hide the record from the learner it is about: they can then read it only through records:read over the owner. Needs records:write over the owner — a self-service submission cannot be hidden — and a status other than pending or rejected.

Responses

StatusDescriptionBody
200The updated record.LearningRecord
400The request body failed validation, or would leave a hidden record in a submission status.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this record but does not hold records:write over it.ErrorResponse
404No visible record has that id.ErrorResponse
409The organization already has a record with that external_id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/records/{record_id} Bearer token

Archive a learning record

Soft-deletes a record by setting archived_at; its evidence and blobs are kept, so it can be un-archived. Requires records:write.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Responses

StatusDescriptionBody
200The archived record.LearningRecord
401The access token is missing or invalid.ErrorResponse
403The caller can read this record but does not hold records:write over it.ErrorResponse
404No visible record has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/records/{record_id}/approve Bearer token

Approve a pending learning record

Moves a learner's pending submission into the real history — completed by default, or in_progress — and stamps who approved it and when. Requires records:write over the record's owner, or a role in the learner's manager chain; a learner cannot approve their own submission.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Request body

application/jsonrequiredApproveLearningRecordRequest
FieldTypeRequiredDescription
statusstringno

What the record becomes on approval: completed (default), or in_progress for learning that is still under way.

one of "completed" | "in_progress" · defaults to "completed"

Responses

StatusDescriptionBody
200The approved record.LearningRecord
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller neither holds records:write over the record's owner nor is in the learner's manager chain.ErrorResponse
404No visible record has that id.ErrorResponse
409The record is not pending — already approved, or archived.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
POST/api/records/{record_id}/reject Bearer token

Reject a pending learning record

Sends a learner's pending submission back with a note saying what to fix, and stamps who rejected it and when. The learner may edit the record and resubmit it to pending, which clears the rejection. Requires records:write over the record's owner, or a role in the learner's manager chain; a learner cannot reject their own submission.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Request body

application/jsonrequiredRejectLearningRecordRequest
FieldTypeRequiredDescription
notestringyes

Why the submission is rejected — what the learner should fix before resubmitting. Shown to the learner on the record.

1–2000 characters

Responses

StatusDescriptionBody
200The rejected record, carrying the note for its learner.LearningRecord
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller neither holds records:write over the record's owner nor is in the learner's manager chain.ErrorResponse
404No visible record has that id.ErrorResponse
409The record is not pending — already decided, or archived.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/records/{record_id}/evidence Bearer token

List a record's evidence

Lists every evidence row of a record, newest first. Requires records:read over the record, being its learner, or — while it is pending — being in its learner's manager chain.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Responses

StatusDescriptionBody
200The record's evidence, newest first.array of SupportingEvidence
401The access token is missing or invalid.ErrorResponse
404No visible record has that id.ErrorResponse
500The database could not be read.ErrorResponse
POST/api/records/{record_id}/evidence Bearer token

Add evidence to a record

Attaches one measurement to a record. For file evidence, upload the file first via the uploads endpoint and pass the resulting pathname here. Requires records:write.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Request body

application/jsonrequiredCreateSupportingEvidenceRequest
FieldTypeRequiredDescription
labelstringno

Human label for the measurement, e.g. "Final theory exam".

1–300 characters

payloadEvidencePayloadyes—
pathnamestringno

For file evidence only: the pathname issued by the uploads endpoint, after the file has been PUT there.

at least 1 character

Responses

StatusDescriptionBody
201The created evidence row.SupportingEvidence
400The payload failed validation, or the uploaded blob is missing or does not match it.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this record but does not hold records:write over it.ErrorResponse
404No visible record has that id.ErrorResponse
409That upload has already been attached to an evidence row.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
POST/api/records/{record_id}/evidence/uploads Bearer token

Request an upload URL for file evidence

Issues a presigned URL to PUT one file straight to private blob storage, scoped to this record, the declared content type and a 25 MB ceiling. Requires records:write.

Parameters

NameInTypeRequiredDescription
record_idpathstring (uuid)yese.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7

Request body

application/jsonrequiredCreateUploadTicketRequest
FieldTypeRequiredDescription
file_namestringyes

1–300 characters

content_typestringyes

one of "application/pdf" | "image/png" | "image/jpeg" | "image/webp"

size_bytesintegeryes

maximum 26214400

Responses

StatusDescriptionBody
201Where to PUT the file, and the evidence id minted for it.UploadTicket
400The request body failed validation.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read this record but does not hold records:write over it.ErrorResponse
404No visible record has that id.ErrorResponse
500The upload URL could not be signed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse
PATCH/api/evidence/{evidence_id} Bearer token

Update an evidence row

Changes an evidence row's label or replaces its payload (same evidence_type; not for files). Requires records:write over the owning record.

Parameters

NameInTypeRequiredDescription
evidence_idpathstring (uuid)yese.g. 0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4

Request body

application/jsonrequiredUpdateSupportingEvidenceRequest
FieldTypeRequiredDescription
labelstring | nullno

Send null to clear the label.

1–300 characters

payloadanyno—

Responses

StatusDescriptionBody
200The updated evidence row.SupportingEvidence
400The payload failed validation, changed type, or replaced a file payload.ErrorResponse
401The access token is missing or invalid.ErrorResponse
403The caller can read the record but does not hold records:write over it.ErrorResponse
404No visible evidence row has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
DELETE/api/evidence/{evidence_id} Bearer token

Delete an evidence row

Removes an evidence row for good — a mistaken measurement is deleted, not archived — and deletes its blob, if it has one. Requires records:write over the owning record.

Parameters

NameInTypeRequiredDescription
evidence_idpathstring (uuid)yese.g. 0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4

Responses

StatusDescriptionBody
204The evidence row (and its blob, if any) is gone.—
401The access token is missing or invalid.ErrorResponse
403The caller can read the record but does not hold records:write over it.ErrorResponse
404No visible evidence row has that id.ErrorResponse
500The database could not be reached, or the write failed.ErrorResponse
GET/api/evidence/{evidence_id}/download Bearer token

Get a download URL for file evidence

Issues a short-lived presigned GET for the evidence row's private blob, after re-checking read permission. Requires records:read over the owning record, being its learner, or — while the record is pending — being in its learner's manager chain.

Parameters

NameInTypeRequiredDescription
evidence_idpathstring (uuid)yese.g. 0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4

Responses

StatusDescriptionBody
200Where to fetch the file from, for the next five minutes.DownloadTicket
401The access token is missing or invalid.ErrorResponse
404No visible evidence row has that id, or it has no file attached.ErrorResponse
500The download URL could not be signed.ErrorResponse
503File storage is not configured on this deployment.ErrorResponse