records
17 endpoints.
/api/records/submissions Bearer tokenSubmit 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.
| Field | Type | Required | Description |
|---|---|---|---|
| record | string | yes | 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 |
| evidence | string | yes | 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 |
| file | string (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
| Status | Description | Body |
|---|---|---|
| 201 | The created record and its evidence rows. | LearningRecordSubmission |
| 400 | A part failed validation, or the file part does not match the declared file evidence. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller does not hold records:write over the owner, and this is not a pending submission about themselves. | ErrorResponse |
| 404 | The organization, department, learner, or a tagged subject does not exist there. | ErrorResponse |
| 409 | The organization already has a record with that external_id. | ErrorResponse |
| 500 | The file could not be stored, or the write failed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/records Bearer tokenList 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| department | query | string | no | Department UUID id or slug, resolved within org (which is then required). |
| learner | query | string (uuid) | no | Only records about this learner (`users.id`). |
| subject | query | string | no | Only records filed under this subject matter: UUID id, or slug resolved within org. |
| status | query | string | no | — |
| evidence_type | query | string | no | Only records carrying at least one evidence row of this type. |
| include_archived | query | string | no | Include archived rows. Defaults to false. |
| limit | query | integer | no | Page size, 1-200. Defaults to 50. |
| cursor | query | string | no | Opaque cursor from a previous page's next_cursor. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | One page of records, and the cursor for the next. | LearningRecordList |
| 400 | A filter did not resolve, or the cursor is malformed. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/records Bearer tokenCreate 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.
| Field | Type | Required | Description |
|---|---|---|---|
| owner_organization_id | string (uuid) | yes | Organization the record belongs to. Always required. |
| owner_department_id | string (uuid) | no | Omit for a record owned by the organization directly. |
| learner_user_id | string (uuid) | no | `users.id` of the learner. Omit to record about yourself — the snapshots below then default to your own profile. |
| learner_display_name | string | no | 1–300 characters |
| learner_email | string | no | 1–300 characters |
| title | string | yes | 1–300 characters |
| description | string | no | at most 5000 characters |
| status | string | no | 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_at | string (date-time) | null | no | 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_at | string (date-time) | null | no | Must be after completed_at when both are set. |
| external_id | string | no | Importer-assigned id; unique per owning organization, so re-runs are idempotent. 1–300 characters |
| subject_matter_ids | array of string (uuid) | no | Subjects to file the record under; must belong to the same organization. at most 100 items · defaults to [] |
| hidden_from_learner | boolean | no | 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
| Status | Description | Body |
|---|---|---|
| 201 | The created record. | LearningRecord |
| 400 | The request body failed validation, a pending submission about yourself omitted completed_at, or a hidden record was given a submission status. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller does not hold records:write over the owner. | ErrorResponse |
| 404 | The organization, department, learner, or a tagged subject does not exist there. | ErrorResponse |
| 409 | The organization already has a record with that external_id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/records/export.csv Bearer tokenExport 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| org | query | string | no | Organization UUID id or slug to filter by (and to resolve slug refs in). |
| department | query | string | no | Department UUID id or slug, resolved within org (which is then required). |
| learner | query | string (uuid) | no | Only records about this learner (`users.id`). |
| subject | query | string | no | Only records filed under this subject matter: UUID id, or slug resolved within org. |
| status | query | string | no | — |
| evidence_type | query | string | no | Only records carrying at least one evidence row of this type. |
| include_archived | query | string | no | Include archived rows. Defaults to false. |
| limit | query | integer | no | The 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
| Status | Description | Body |
|---|---|---|
| 200 | The CSV file, served as an attachment named `learning-records-<date>.csv`. | — |
| 400 | A filter did not resolve, or the limit is above the caller's ceiling. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 422 | More records match than the limit allows; nothing is exported. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/records/{record_id}/permanent-deletion Bearer tokenPreview 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | What the deletion would take with it. | DeletionImpact |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller does not hold records:delete over the record's owner. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/records/{record_id}/permanent-deletion Bearer tokenPermanently 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | The record and its evidence are gone. | — |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller does not hold records:delete over the record's owner. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/records/{record_id} Bearer tokenGet 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The record. | LearningRecord |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/records/{record_id} Bearer tokenUpdate 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
| Field | Type | Required | Description |
|---|---|---|---|
| owner_department_id | string (uuid) | null | no | Move the record to another department of the same organization; null hands it to the organization directly. |
| learner_display_name | string | null | no | 1–300 characters |
| learner_email | string | null | no | 1–300 characters |
| title | string | no | 1–300 characters |
| description | string | null | no | at most 5000 characters |
| status | string | no | 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_at | string (date-time) | null | no | — |
| expires_at | string (date-time) | null | no | — |
| external_id | string | null | no | 1–300 characters |
| subject_matter_ids | array of string (uuid) | no | When present, replaces the record's whole subject set. at most 100 items |
| hidden_from_learner | boolean | no | 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
| Status | Description | Body |
|---|---|---|
| 200 | The updated record. | LearningRecord |
| 400 | The request body failed validation, or would leave a hidden record in a submission status. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this record but does not hold records:write over it. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 409 | The organization already has a record with that external_id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/records/{record_id} Bearer tokenArchive 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The archived record. | LearningRecord |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this record but does not hold records:write over it. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/records/{record_id}/approve Bearer tokenApprove 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | no | 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
| Status | Description | Body |
|---|---|---|
| 200 | The approved record. | LearningRecord |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller neither holds records:write over the record's owner nor is in the learner's manager chain. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 409 | The record is not pending — already approved, or archived. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/records/{record_id}/reject Bearer tokenReject 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
| Field | Type | Required | Description |
|---|---|---|---|
| note | string | yes | Why the submission is rejected — what the learner should fix before resubmitting. Shown to the learner on the record. 1–2000 characters |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The rejected record, carrying the note for its learner. | LearningRecord |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller neither holds records:write over the record's owner nor is in the learner's manager chain. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 409 | The record is not pending — already decided, or archived. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/records/{record_id}/evidence Bearer tokenList 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The record's evidence, newest first. | array of SupportingEvidence |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 500 | The database could not be read. | ErrorResponse |
/api/records/{record_id}/evidence Bearer tokenAdd 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | no | Human label for the measurement, e.g. "Final theory exam". 1–300 characters |
| payload | EvidencePayload | yes | — |
| pathname | string | no | For file evidence only: the pathname issued by the uploads endpoint, after the file has been PUT there. at least 1 character |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The created evidence row. | SupportingEvidence |
| 400 | The payload failed validation, or the uploaded blob is missing or does not match it. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this record but does not hold records:write over it. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 409 | That upload has already been attached to an evidence row. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/records/{record_id}/evidence/uploads Bearer tokenRequest 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| record_id | path | string (uuid) | yes | e.g. 6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7 |
| Field | Type | Required | Description |
|---|---|---|---|
| file_name | string | yes | 1–300 characters |
| content_type | string | yes | one of "application/pdf" | "image/png" | "image/jpeg" | "image/webp" |
| size_bytes | integer | yes | maximum 26214400 |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Where to PUT the file, and the evidence id minted for it. | UploadTicket |
| 400 | The request body failed validation. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read this record but does not hold records:write over it. | ErrorResponse |
| 404 | No visible record has that id. | ErrorResponse |
| 500 | The upload URL could not be signed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |
/api/evidence/{evidence_id} Bearer tokenUpdate 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| evidence_id | path | string (uuid) | yes | e.g. 0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4 |
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | null | no | Send null to clear the label. 1–300 characters |
| payload | any | no | — |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The updated evidence row. | SupportingEvidence |
| 400 | The payload failed validation, changed type, or replaced a file payload. | ErrorResponse |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read the record but does not hold records:write over it. | ErrorResponse |
| 404 | No visible evidence row has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/evidence/{evidence_id} Bearer tokenDelete 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| evidence_id | path | string (uuid) | yes | e.g. 0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4 |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | The evidence row (and its blob, if any) is gone. | — |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 403 | The caller can read the record but does not hold records:write over it. | ErrorResponse |
| 404 | No visible evidence row has that id. | ErrorResponse |
| 500 | The database could not be reached, or the write failed. | ErrorResponse |
/api/evidence/{evidence_id}/download Bearer tokenGet 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| evidence_id | path | string (uuid) | yes | e.g. 0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Where to fetch the file from, for the next five minutes. | DownloadTicket |
| 401 | The access token is missing or invalid. | ErrorResponse |
| 404 | No visible evidence row has that id, or it has no file attached. | ErrorResponse |
| 500 | The download URL could not be signed. | ErrorResponse |
| 503 | File storage is not configured on this deployment. | ErrorResponse |