{"openapi":"3.1.0","info":{"title":"Botree LRS","version":"0.25.4"},"servers":[{"url":"https://gtaa.botreeinc.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"OIDC access token issued by one of this deployment's registered sign-in providers."}},"schemas":{"TokenInfo":{"type":"object","properties":{"active":{"type":"boolean","enum":[true]},"subject":{"type":"string","example":"usr_123"},"client_id":{"type":"string","example":"botree-lrs"},"issuer":{"type":"string","example":"https://auth.botreeinc.com"},"audience":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"},"example":["lrs:read"]},"issued_at":{"type":"integer","description":"Seconds since the epoch."},"expires_at":{"type":"integer","description":"Seconds since the epoch."},"validated_by":{"type":"string","enum":["jwks","introspection"],"description":"Whether the token was validated locally against the JWKS or by introspection."}},"required":["active","audience","scopes","validated_by"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string","example":"invalid_token"},"error_description":{"type":"string","example":"The access token has expired"}},"required":["error","error_description"]},"Organization":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","slug","name","description","created_at"]},"CreateOrganizationRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"acme-corporation","description":"Unique across all organizations. Slugs are lowercase letters and digits in words separated by single hyphens, like customer-success."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"ACME Corporation"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."}},"required":["slug","name"]},"UpdateOrganizationRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","description":"Unique across all organizations. Slugs are lowercase letters and digits in words separated by single hyphens, like customer-success."},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null or empty to clear the description."}}},"Department":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"parent_department_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","organization_id","slug","name","description","parent_department_id","created_at"]},"CreateDepartmentRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"quality-assurance","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like customer-success."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Quality Assurance"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."},"parent_department_id":{"type":"string","format":"uuid","description":"Nests the new department under one in the same organization."}},"required":["slug","name"]},"UpdateDepartmentRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like customer-success."},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null or empty to clear the description."},"parent_department_id":{"type":["string","null"],"format":"uuid","description":"Moves the department under another in the same organization — never itself or anything nested beneath it. Send null to make it top level."}}},"DepartmentRole":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"department_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"permissions":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","department_id","slug","name","description","permissions","created_at"]},"CreateDepartmentRoleRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"reviewer","description":"Unique within the department. Slugs are lowercase letters and digits in words separated by single hyphens, like customer-success."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Reviewer"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."},"permissions":{"type":"array","items":{"type":"string","enum":["records:read","records:write","subjects:read","subjects:write","certifications:read","certifications:write","quizzes:read","quizzes:write","external-lms:read","external-lms:write","scorm:read","scorm:write","directory:write"]},"maxItems":13,"default":[],"description":"The permissions the role grants, from the set the code understands. Duplicates are collapsed; an empty array grants nothing.","example":["records:read","records:write"]}},"required":["slug","name"]},"UpdateDepartmentRoleRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","description":"Unique within the department. Slugs are lowercase letters and digits in words separated by single hyphens, like customer-success."},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null or empty to clear the description."},"permissions":{"type":"array","items":{"type":"string","enum":["records:read","records:write","subjects:read","subjects:write","certifications:read","certifications:write","quizzes:read","quizzes:write","external-lms:read","external-lms:write","scorm:read","scorm:write","directory:write"]},"maxItems":13,"description":"Replaces the whole permission set, from the set the code understands. Duplicates are collapsed; an empty array grants nothing.","example":["records:read","records:write"]}}},"DirectoryImportSummary":{"type":"object","properties":{"total_rows":{"type":"integer","description":"Data rows in the file, not counting the header.","example":25},"added_count":{"type":"integer","description":"Members added to the directory by this import.","example":23},"skipped_count":{"type":"integer","description":"Rows skipped because their subject was already in the directory.","example":2},"skipped_subjects":{"type":"array","items":{"type":"string"},"description":"The file's subjects whose accounts were already in the directory, in file order; their existing accounts and profiles are untouched.","example":["auth0|507f1f77bcf86cd799439011"]}},"required":["total_rows","added_count","skipped_count","skipped_subjects"]},"DirectoryImportForm":{"type":"object","properties":{"provider_id":{"type":"string","minLength":1,"description":"Sign-in provider UUID id or slug; every row's user_id is the OIDC `sub` claim the member will present at this provider.","example":"acme-sso"},"file":{"type":"string","format":"binary","description":"CSV file, at most 1 MB: a header row naming a `user_id` (the OIDC `sub` claim at the named provider; `user_subject`, `subject` and `sub` are accepted too), `name` (or `display_name`) and `email` column in any order, then one member per row. Header matching is case-insensitive and extra columns are ignored. A member's name and email cells may be left blank; they seed a newly created account's profile."}},"required":["provider_id","file"]},"DirectoryUser":{"type":"object","properties":{"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"display_name":{"type":["string","null"],"description":"The member's canonical profile, refreshed from their sign-ins."},"email":{"type":["string","null"]},"kind":{"type":"string","enum":["human","machine"],"description":"`machine` marks accounts created from client-credentials tokens."},"active_role_count":{"type":"integer","description":"Roles the member currently holds here; expired grants are not counted.","example":2},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["organization_id","user_id","display_name","email","kind","active_role_count","created_at"]},"AddDirectoryUserRequest":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid","description":"The member's account id (`users.id`). Mutually exclusive with provider_id/subject."},"provider_id":{"type":"string","minLength":1,"description":"Sign-in provider UUID id or slug.","example":"acme-sso"},"subject":{"type":"string","minLength":1,"maxLength":255,"description":"OIDC `sub` claim the member will present at that provider.","example":"auth0|507f1f77bcf86cd799439011"},"display_name":{"type":"string","minLength":1,"maxLength":300,"description":"Starting profile for a newly created account; ignored for an existing one."},"email":{"type":"string","minLength":1,"maxLength":300}}},"DirectoryUserWithRoles":{"type":"object","properties":{"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"display_name":{"type":["string","null"],"description":"The member's canonical profile, refreshed from their sign-ins."},"email":{"type":["string","null"]},"kind":{"type":"string","enum":["human","machine"],"description":"`machine` marks accounts created from client-credentials tokens."},"active_role_count":{"type":"integer","description":"Roles the member currently holds here; expired grants are not counted.","example":2},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"roles":{"type":"array","items":{"$ref":"#/components/schemas/DirectoryUserRole"},"description":"Every grant the member holds here, expired ones included."}},"required":["organization_id","user_id","display_name","email","kind","active_role_count","created_at","roles"]},"DirectoryUserRole":{"type":"object","properties":{"department_id":{"type":"string","format":"uuid"},"department_slug":{"type":"string"},"department_name":{"type":"string"},"role_id":{"type":"string","format":"uuid"},"role_slug":{"type":"string"},"role_name":{"type":"string"},"role_description":{"type":["string","null"]},"permissions":{"type":"array","items":{"type":"string"},"example":["records:read","records:write"]},"granted_by":{"type":["string","null"],"description":"`users.id` of whoever granted the role; null when unrecorded."},"granted_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the grant lapses; null for an open-ended one."}},"required":["department_id","department_slug","department_name","role_id","role_slug","role_name","role_description","permissions","granted_by","granted_at","expires_at"]},"AssignDirectoryUserRoleRequest":{"type":"object","properties":{"department_id":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within the organization.","example":"quality-assurance"},"role_id":{"type":"string","minLength":1,"description":"Role UUID id or slug, resolved within the department.","example":"reviewer"},"expires_at":{"type":"string","format":"date-time","description":"When the grant lapses; omit for an open-ended one. Must be in the future."}},"required":["department_id","role_id"]},"UpdateDirectoryUserRoleRequest":{"type":"object","properties":{"expires_at":{"type":["string","null"],"format":"date-time","description":"New expiry of the grant; null makes it open-ended."}},"required":["expires_at"]},"RoleManagers":{"type":"object","properties":{"managers":{"type":"array","items":{"$ref":"#/components/schemas/RoleManagerEdge"},"description":"The roles managing this role, each edge's far side resolved."},"subordinates":{"type":"array","items":{"$ref":"#/components/schemas/RoleManagerEdge"},"description":"The roles this role manages, each edge's far side resolved."}},"required":["managers","subordinates"]},"RoleManagerEdge":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"role":{"$ref":"#/components/schemas/RoleManagerRef"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","organization_id","role","created_at"]},"RoleManagerRef":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid"},"role_slug":{"type":"string"},"role_name":{"type":"string"},"department_id":{"type":"string","format":"uuid"},"department_slug":{"type":"string"},"department_name":{"type":"string"}},"required":["role_id","role_slug","role_name","department_id","department_slug","department_name"]},"CreateRoleManagerRequest":{"type":"object","properties":{"manager_role_id":{"type":"string","format":"uuid","description":"The role to make a manager of this one. Any role of the same organization except this role itself, or one that would close a cycle."}},"required":["manager_role_id"]},"OrganizationSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired and should not gain new children."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","slug","name","description","archived_at","created_at"]},"OrganizationWithDepartments":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired and should not gain new children."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"departments":{"type":"array","items":{"$ref":"#/components/schemas/DepartmentSummary"},"description":"Every department in the organization, nested ones included, ordered by name."}},"required":["id","slug","name","description","archived_at","created_at","departments"]},"DepartmentSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"parent_department_id":{"type":["string","null"],"format":"uuid"},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired and should not gain new children."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","organization_id","slug","name","description","parent_department_id","archived_at","created_at"]},"DepartmentRoleSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"department_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"permissions":{"type":"array","items":{"type":"string"},"example":["records:read","records:write"]},"member_count":{"type":"integer","description":"Users currently holding the role; expired grants are not counted.","example":3},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","department_id","slug","name","description","permissions","member_count","created_at"]},"SignInProviderList":{"type":"object","properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/SignInProvider"}}},"required":["providers"]},"SignInProvider":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string","example":"acme-sso"},"display_name":{"type":"string","description":"Label shown on the sign-in picker.","example":"Acme SSO"},"issuer_url":{"type":"string","example":"https://auth.acme.com"},"client_id":{"type":"string"},"enabled":{"type":"boolean","description":"Disabled providers keep their identities but accept no sign-ins or tokens."},"client_auth_method":{"type":"string","enum":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"]},"client_secret_env":{"type":["string","null"],"description":"NAME of the env var holding the client secret; the secret itself never leaves the environment.","example":"OIDC_ACME_CLIENT_SECRET"},"client_private_key_env":{"type":["string","null"],"description":"NAME of the env var holding the PKCS#8 private key, for private_key_jwt."},"client_assertion_algorithm":{"type":["string","null"]},"client_key_id":{"type":["string","null"]},"audience":{"type":["string","null"],"description":"Expected `aud` of incoming access tokens; null skips the audience check."},"login_scope":{"type":"string","example":"openid profile email"},"access_token_algorithms":{"type":"array","items":{"type":"string"},"description":"Accepted JWS algorithms for incoming access tokens; empty means the defaults.","example":["RS256"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","slug","display_name","issuer_url","client_id","enabled","client_auth_method","client_secret_env","client_private_key_env","client_assertion_algorithm","client_key_id","audience","login_scope","access_token_algorithms","created_at","updated_at"]},"CreateSignInProviderRequest":{"type":"object","properties":{"slug":{"type":"string","maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"display_name":{"type":"string","minLength":1,"maxLength":200},"issuer_url":{"type":"string","maxLength":500,"format":"uri"},"client_id":{"type":"string","minLength":1,"maxLength":500},"client_auth_method":{"type":"string","enum":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"]},"client_secret_env":{"type":"string","maxLength":200,"pattern":"^[A-Z_][A-Z0-9_]*$"},"client_private_key_env":{"type":"string","maxLength":200,"pattern":"^[A-Z_][A-Z0-9_]*$"},"client_assertion_algorithm":{"type":"string","minLength":1,"maxLength":20},"client_key_id":{"type":"string","minLength":1,"maxLength":200},"audience":{"type":"string","minLength":1,"maxLength":500},"login_scope":{"type":"string","minLength":1,"maxLength":500,"description":"Defaults to \"openid profile email\"."},"access_token_algorithms":{"type":"array","items":{"type":"string","minLength":1,"maxLength":20},"maxItems":20,"description":"Omit to accept the default algorithm set."}},"required":["slug","display_name","issuer_url","client_id","client_auth_method"]},"UpdateSignInProviderRequest":{"type":"object","properties":{"slug":{"type":"string","maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"display_name":{"type":"string","minLength":1,"maxLength":200},"issuer_url":{"type":"string","maxLength":500,"format":"uri"},"client_id":{"type":"string","minLength":1,"maxLength":500},"enabled":{"type":"boolean"},"client_auth_method":{"type":"string","enum":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt","none"]},"client_secret_env":{"type":["string","null"],"maxLength":200,"pattern":"^[A-Z_][A-Z0-9_]*$","description":"Omit to keep the current value; null clears it."},"client_private_key_env":{"type":["string","null"],"maxLength":200,"pattern":"^[A-Z_][A-Z0-9_]*$"},"client_assertion_algorithm":{"type":["string","null"],"minLength":1,"maxLength":20},"client_key_id":{"type":["string","null"],"minLength":1,"maxLength":200},"audience":{"type":["string","null"],"minLength":1,"maxLength":500},"login_scope":{"type":"string","minLength":1,"maxLength":500},"access_token_algorithms":{"type":"array","items":{"type":"string","minLength":1,"maxLength":20},"maxItems":20}}},"UserProfile":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"],"description":"Canonical profile, refreshed from ID-token claims at each sign-in."},"email":{"type":["string","null"]},"kind":{"type":"string","enum":["human","machine"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"identities":{"type":"array","items":{"$ref":"#/components/schemas/UserIdentity"},"description":"The account's sign-in methods, oldest first."}},"required":["id","display_name","email","kind","created_at","identities"]},"UserIdentity":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider_id":{"type":"string","format":"uuid"},"provider_slug":{"type":"string","example":"acme-sso"},"provider_name":{"type":"string","example":"Acme SSO"},"provider_enabled":{"type":"boolean","description":"A disabled provider's identities remain linked but cannot sign in."},"subject":{"type":"string","description":"The `sub` claim this identity presents at its provider.","example":"auth0|507f1f77bcf86cd799439011"},"last_authenticated_at":{"type":["string","null"],"format":"date-time","description":"When a sign-in or verified token last presented this identity."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","provider_id","provider_slug","provider_name","provider_enabled","subject","last_authenticated_at","created_at"]},"UserAccount":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"]},"email":{"type":["string","null"]},"kind":{"type":"string","enum":["human","machine"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","display_name","email","kind","created_at","updated_at"]},"UpdateUserRequest":{"type":"object","properties":{"display_name":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"The name shown for the account everywhere; null clears it.","example":"Ada Lovelace"}}},"AttachIdentityRequest":{"type":"object","properties":{"provider_id":{"type":"string","minLength":1,"description":"Sign-in provider UUID id or slug.","example":"acme-sso"},"subject":{"type":"string","minLength":1,"maxLength":255,"description":"The `sub` claim the account's owner presents at that provider."}},"required":["provider_id","subject"]},"MergeUsersResponse":{"type":"object","properties":{"merged_user_id":{"type":"string","format":"uuid","description":"The absorbed (now tombstoned) account."},"into_user_id":{"type":"string","format":"uuid"}},"required":["merged_user_id","into_user_id"]},"MergeUsersRequest":{"type":"object","properties":{"into_user_id":{"type":"string","format":"uuid","description":"The surviving account everything is repointed at."}},"required":["into_user_id"]},"SubjectMatterList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SubjectMatter"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"SubjectMatter":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the subject."},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","owner_organization_id","owner_department_id","slug","name","description","archived_at","created_at"]},"CreateSubjectMatterRequest":{"type":"object","properties":{"owner_organization_id":{"type":"string","format":"uuid","description":"Organization the subject belongs to. Always required."},"owner_department_id":{"type":"string","format":"uuid","description":"Omit for a subject owned by the organization directly."},"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"forklift-safety","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like forklift-safety."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Forklift safety"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."}},"required":["owner_organization_id","slug","name"]},"UpdateSubjectMatterRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the description."}}},"LearningRecordSubmission":{"type":"object","properties":{"record":{"$ref":"#/components/schemas/LearningRecord"},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/SupportingEvidence"},"description":"The created evidence rows, in the order they were submitted."}},"required":["record","evidence"]},"LearningRecord":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the record."},"learner_user_id":{"type":"string","format":"uuid","description":"`users.id` of the learner the record is about.","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"learner_display_name":{"type":["string","null"],"description":"Snapshot taken at write time; the IdP is not queryable for it later."},"learner_email":{"type":["string","null"]},"title":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["pending","rejected","in_progress","completed","expired","revoked"]},"completed_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"external_id":{"type":["string","null"],"description":"Importer-assigned id, unique per owning organization."},"recorded_by":{"type":["string","null"],"format":"uuid","description":"`users.id` of the writer, which is not always the learner."},"approved_by":{"type":["string","null"],"format":"uuid","description":"`users.id` of the manager who approved the record out of pending; null otherwise."},"approved_at":{"type":["string","null"],"format":"date-time"},"rejected_by":{"type":["string","null"],"format":"uuid","description":"`users.id` of the manager who rejected the record back to its learner; set exactly while the status is rejected."},"rejected_at":{"type":["string","null"],"format":"date-time"},"rejection_note":{"type":["string","null"],"description":"The rejecter's note: what to fix before resubmitting."},"subjects":{"type":"array","items":{"$ref":"#/components/schemas/RecordSubjectRef"}},"evidence_count":{"type":"integer"},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","owner_organization_id","owner_department_id","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","subjects","evidence_count","archived_at","created_at"]},"RecordSubjectRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"archived_at":{"type":["string","null"],"format":"date-time"}},"required":["id","slug","name","archived_at"]},"SupportingEvidence":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"learning_record_id":{"type":"string","format":"uuid"},"evidence_type":{"type":"string","enum":["score","date_range","quantity","url","file","pass_fail","note"]},"label":{"type":["string","null"]},"data":{"$ref":"#/components/schemas/EvidencePayload"},"occurred_at":{"type":["string","null"],"format":"date-time"},"period_start":{"type":["string","null"],"format":"date-time"},"period_end":{"type":["string","null"],"format":"date-time"},"numeric_value":{"type":["number","null"]},"numeric_unit":{"type":["string","null"]},"boolean_value":{"type":["boolean","null"]},"text_value":{"type":["string","null"]},"blob_pathname":{"type":["string","null"],"description":"Where the private blob lives, for file evidence. Not itself a URL."},"recorded_by":{"type":["string","null"],"format":"uuid","description":"`users.id` of the writer."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","learning_record_id","evidence_type","label","data","occurred_at","period_start","period_end","numeric_value","numeric_unit","boolean_value","text_value","blob_pathname","recorded_by","created_at"]},"EvidencePayload":{"oneOf":[{"type":"object","properties":{"evidence_type":{"type":"string","enum":["score"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"score":{"type":"number"},"max":{"type":"number","exclusiveMinimum":0},"scale":{"type":["string","null"],"minLength":1}},"required":["evidence_type","score","max"]},{"type":"object","properties":{"evidence_type":{"type":"string","enum":["date_range"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"}},"required":["evidence_type","start","end"]},{"type":"object","properties":{"evidence_type":{"type":"string","enum":["quantity"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"value":{"type":"number"},"unit":{"type":"string","minLength":1}},"required":["evidence_type","value","unit"]},{"type":"object","properties":{"evidence_type":{"type":"string","enum":["url"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"url":{"type":"string","format":"uri"},"label":{"type":["string","null"],"minLength":1}},"required":["evidence_type","url"]},{"type":"object","properties":{"evidence_type":{"type":"string","enum":["file"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"file_name":{"type":"string","minLength":1},"content_type":{"type":"string","enum":["application/pdf","image/png","image/jpeg","image/webp"]},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":26214400},"checksum":{"type":["string","null"],"minLength":1}},"required":["evidence_type","file_name","content_type","size_bytes"]},{"type":"object","properties":{"evidence_type":{"type":"string","enum":["pass_fail"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"passed":{"type":"boolean"},"criteria":{"type":["string","null"],"minLength":1}},"required":["evidence_type","passed"]},{"type":"object","properties":{"evidence_type":{"type":"string","enum":["note"]},"occurred_at":{"type":["string","null"],"format":"date-time","description":"When the thing being evidenced happened, if it happened at a point in time."},"text":{"type":"string","minLength":1}},"required":["evidence_type","text"]}]},"LearningRecordSubmissionForm":{"type":"object","properties":{"record":{"type":"string","minLength":1,"description":"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).","example":"{\"owner_organization_id\":\"1b671a64-40d5-491e-99b0-da01ff1f3341\",\"title\":\"Simulator session\",\"status\":\"pending\",\"completed_at\":\"2026-01-01T00:00:00.000Z\",\"subject_matter_ids\":[\"6b3f7a52-8a20-4f0e-9be1-0f6cbd2f2f5c\"]}"},"evidence":{"type":"string","minLength":1,"description":"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.","example":"[{\"label\":\"Session length\",\"payload\":{\"evidence_type\":\"quantity\",\"value\":2.5,\"unit\":\"hours\"}}]"},"file":{"type":"string","format":"binary","description":"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."}},"required":["record","evidence"]},"LearningRecordList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/LearningRecord"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"CreateLearningRecordRequest":{"type":"object","properties":{"owner_organization_id":{"type":"string","format":"uuid","description":"Organization the record belongs to. Always required."},"owner_department_id":{"type":"string","format":"uuid","description":"Omit for a record owned by the organization directly."},"learner_user_id":{"type":"string","format":"uuid","description":"`users.id` of the learner. Omit to record about yourself — the snapshots below then default to your own profile.","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"learner_display_name":{"type":"string","minLength":1,"maxLength":300},"learner_email":{"type":"string","minLength":1,"maxLength":300},"title":{"type":"string","minLength":1,"maxLength":300,"example":"Forklift operator certification"},"description":{"type":"string","maxLength":5000},"status":{"type":"string","enum":["pending","in_progress","completed","expired","revoked"],"default":"completed","description":"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."},"completed_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time","description":"Must be after completed_at when both are set."},"external_id":{"type":"string","minLength":1,"maxLength":300,"description":"Importer-assigned id; unique per owning organization, so re-runs are idempotent."},"subject_matter_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":100,"default":[],"description":"Subjects to file the record under; must belong to the same organization."}},"required":["owner_organization_id","title"]},"UpdateLearningRecordRequest":{"type":"object","properties":{"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Move the record to another department of the same organization; null hands it to the organization directly."},"learner_display_name":{"type":["string","null"],"minLength":1,"maxLength":300},"learner_email":{"type":["string","null"],"minLength":1,"maxLength":300},"title":{"type":"string","minLength":1,"maxLength":300},"description":{"type":["string","null"],"maxLength":5000},"status":{"type":"string","enum":["pending","in_progress","completed","expired","revoked"],"description":"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."},"completed_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"external_id":{"type":["string","null"],"minLength":1,"maxLength":300},"subject_matter_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":100,"description":"When present, replaces the record's whole subject set."}}},"ApproveLearningRecordRequest":{"type":"object","properties":{"status":{"type":"string","enum":["completed","in_progress"],"default":"completed","description":"What the record becomes on approval: completed (default), or in_progress for learning that is still under way."}}},"RejectLearningRecordRequest":{"type":"object","properties":{"note":{"type":"string","minLength":1,"maxLength":2000,"description":"Why the submission is rejected — what the learner should fix before resubmitting. Shown to the learner on the record.","example":"The simulator hours need the instructor's session log attached."}},"required":["note"]},"CreateSupportingEvidenceRequest":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":300,"description":"Human label for the measurement, e.g. \"Final theory exam\"."},"payload":{"$ref":"#/components/schemas/EvidencePayload"},"pathname":{"type":"string","minLength":1,"description":"For file evidence only: the pathname issued by the uploads endpoint, after the file has been PUT there."}},"required":["payload"]},"UploadTicket":{"type":"object","properties":{"evidence_id":{"type":"string","format":"uuid","description":"Minted for the future evidence row; embedded in the pathname."},"pathname":{"type":"string"},"upload_url":{"type":"string","description":"PUT the file bytes here, with the declared content type."},"expires_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["evidence_id","pathname","upload_url","expires_at"]},"CreateUploadTicketRequest":{"type":"object","properties":{"file_name":{"type":"string","minLength":1,"maxLength":300},"content_type":{"type":"string","enum":["application/pdf","image/png","image/jpeg","image/webp"]},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":26214400}},"required":["file_name","content_type","size_bytes"]},"UpdateSupportingEvidenceRequest":{"type":"object","properties":{"label":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"Send null to clear the label."},"payload":{"allOf":[{"$ref":"#/components/schemas/EvidencePayload"},{"description":"Replaces the measurement. Must keep the row's evidence_type; file payloads cannot be replaced — delete the row and upload again instead."}]}}},"DownloadTicket":{"type":"object","properties":{"url":{"type":"string","description":"Short-lived presigned GET for the private blob. Not stored, not logged."},"expires_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"file_name":{"type":"string"},"content_type":{"type":"string"}},"required":["url","expires_at","file_name","content_type"]},"UserCertificationStatusList":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid"},"items":{"type":"array","items":{"$ref":"#/components/schemas/UserCertificationStatus"}}},"required":["user_id","items"]},"UserCertificationStatus":{"type":"object","properties":{"certification_type":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"awarding_mode":{"type":"string","enum":["automatic","approval"]}},"required":["id","owner_organization_id","owner_department_id","slug","name","description","awarding_mode"]},"subject_matter":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"required_by_roles":{"type":"array","items":{"$ref":"#/components/schemas/CertificationRequiredByRole"},"description":"Every unexpired role grant of the user that requires this type."},"current_award":{"$ref":"#/components/schemas/CertificationAward"},"due_at":{"type":["string","null"],"format":"date-time","description":"When the certification (or the next recertification) falls due; null when no initial due date is set or inherited, and no recert scheme applies."},"due_source":{"type":["string","null"],"enum":["initial","role","recert",null],"description":"initial: the admin-set per-person due date. role: inherited from the earliest role-level initial due date across the requiring roles. recert: computed from the current award and the strictest recert scheme across the requiring roles."},"overdue":{"type":"boolean"},"rules":{"type":"array","items":{"$ref":"#/components/schemas/CertificationRuleProgress"},"description":"Progress per proof rule, evaluated since the current award (or over all history while uncertified)."},"all_rules_satisfied":{"type":"boolean"},"pending_request_id":{"type":["string","null"],"format":"uuid","description":"The user's pending request for this type, if one is open."},"pending_submission_count":{"type":"integer","description":"The user's learning-record submissions on the type's subject still awaiting a manager's approval, within the evaluation window — counting only submissions whose approval would advance at least one of the type's rules. The progress they would grant is carried per rule as pending_current / pending_evidence_present."}},"required":["certification_type","subject_matter","required_by_roles","current_award","due_at","due_source","overdue","rules","all_rules_satisfied","pending_request_id","pending_submission_count"]},"CertificationRequiredByRole":{"type":"object","properties":{"link_id":{"type":"string","format":"uuid"},"role_id":{"type":"string","format":"uuid"},"role_name":{"type":"string"},"department_id":{"type":"string","format":"uuid"},"department_name":{"type":"string"},"organization_id":{"type":"string","format":"uuid"},"organization_name":{"type":"string"},"recert_scheme":{"type":"string","enum":["none","anniversary","fixed_calendar"]},"recert_interval_months":{"type":["number","null"]},"recert_anchor_month":{"type":["number","null"]},"recert_anchor_day":{"type":["number","null"]},"initial_due_at":{"type":["string","null"],"format":"date-time","description":"The link's role-level initial due date; null when the role sets no deadline."}},"required":["link_id","role_id","role_name","department_id","department_name","organization_id","organization_name","recert_scheme","recert_interval_months","recert_anchor_month","recert_anchor_day","initial_due_at"]},"CertificationAward":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"certification_type_id":{"type":"string","format":"uuid"},"award_kind":{"type":"string","enum":["claimed","approved","granted"]},"awarded_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"awarded_by":{"type":["string","null"],"format":"uuid","description":"Null on a self-claimed award."},"revoked_at":{"type":["string","null"],"format":"date-time"},"revoked_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","organization_id","user_id","certification_type_id","award_kind","awarded_at","awarded_by","revoked_at","revoked_by","created_at"],"description":"The latest non-revoked award, or null while the user is uncertified."},"CertificationRuleProgress":{"type":"object","properties":{"rule_id":{"type":"string","format":"uuid"},"rule_kind":{"type":"string","enum":["subject_hours","subject_record_count","evidence_document","manual_sign_off","quiz_pass","external_course_pass"]},"label":{"type":["string","null"]},"current":{"type":["number","null"],"description":"Hours or completed records accumulated in the evaluation window, for the threshold rule kinds; null for the others."},"pending_current":{"type":["number","null"],"description":"Hours or records the user's still-pending submissions in the window would add once a manager approves them, for the threshold rule kinds; null for the others."},"target":{"type":["number","null"],"description":"The rule's threshold, for the threshold rule kinds; null for the others."},"evidence_type":{"type":["string","null"],"enum":["score","date_range","quantity","url","file","pass_fail","note",null],"description":"The evidence type an evidence_document rule looks for."},"evidence_present":{"type":["boolean","null"],"description":"Whether a completed record in the window carries that evidence; null for other kinds."},"pending_evidence_present":{"type":["boolean","null"],"description":"Whether a still-pending submission in the window carries that evidence; null for other kinds."},"last_sign_off_at":{"type":["string","null"],"format":"date-time","description":"Most recent sign-off for a manual_sign_off rule, window regardless."},"quiz_id":{"type":["string","null"],"format":"uuid","description":"The quiz a quiz_pass rule requires; null for other kinds."},"quiz_slug":{"type":["string","null"]},"quiz_name":{"type":["string","null"]},"last_quiz_pass_at":{"type":["string","null"],"format":"date-time","description":"Most recent passing attempt for a quiz_pass rule, window regardless."},"external_course_id":{"type":["string","null"],"format":"uuid","description":"Catalogue id of the course an external_course_pass rule requires; null for other kinds."},"external_course_name":{"type":["string","null"]},"external_course_code":{"type":["string","null"],"description":"The LMS's own identifier of that course."},"external_server_name":{"type":["string","null"]},"last_course_pass_at":{"type":["string","null"],"format":"date-time","description":"Most recent passing grade for an external_course_pass rule, window regardless."},"satisfied":{"type":"boolean"}},"required":["rule_id","rule_kind","label","current","pending_current","target","evidence_type","evidence_present","pending_evidence_present","last_sign_off_at","quiz_id","quiz_slug","quiz_name","last_quiz_pass_at","external_course_id","external_course_name","external_course_code","external_server_name","last_course_pass_at","satisfied"]},"CertificationRequest":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"user_display_name":{"type":["string","null"],"description":"The requester's profile name, when their account carries one."},"user_email":{"type":["string","null"]},"certification_type_id":{"type":"string","format":"uuid"},"certification_type":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"awarding_mode":{"type":"string","enum":["automatic","approval"]}},"required":["id","slug","name","awarding_mode"],"description":"The requested certification type, for display."},"status":{"type":"string","enum":["pending","approved","rejected","withdrawn"]},"message":{"type":["string","null"]},"decided_by":{"type":["string","null"],"format":"uuid"},"decided_at":{"type":["string","null"],"format":"date-time"},"decision_note":{"type":["string","null"]},"resulting_certification_id":{"type":["string","null"],"format":"uuid","description":"The award an approval produced; set exactly when status is approved."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","organization_id","user_id","user_display_name","user_email","certification_type_id","certification_type","status","message","decided_by","decided_at","decision_note","resulting_certification_id","created_at"]},"RequestCertificationRequest":{"type":"object","properties":{"message":{"type":"string","maxLength":2000,"description":"Optional note to whoever decides the request."}}},"DecideCertificationRequestRequest":{"type":"object","properties":{"decision_note":{"type":"string","maxLength":2000,"description":"Optional note recorded with the decision, shown to the requester."}}},"CertificationRequestList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CertificationRequest"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"GrantCertificationRequest":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid","description":"`users.id` of the user the certification is granted to."},"awarded_at":{"type":"string","format":"date-time","description":"Omit for now; set to backdate a grant, e.g. one imported from paper."}},"required":["user_id"]},"CertificationAwardList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CertificationAward"}}},"required":["items"]},"CertificationDueDateList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CertificationDueDate"}}},"required":["items"]},"CertificationDueDate":{"type":"object","properties":{"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"certification_type_id":{"type":"string","format":"uuid"},"initial_due_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z","description":"When the user's *initial* certification falls due; recerts are computed. Overrides any role-level initial due date."},"set_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["organization_id","user_id","certification_type_id","initial_due_at","set_by","created_at"]},"SetCertificationDueDateRequest":{"type":"object","properties":{"initial_due_at":{"type":"string","format":"date-time","description":"When this user's initial certification falls due. Takes precedence over any role-level initial due date, earlier or later alike."}},"required":["initial_due_at"]},"CertificationSignOff":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"requirement_id":{"type":"string","format":"uuid"},"certification_type_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"signed_off_by":{"type":"string","format":"uuid"},"signed_off_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"note":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","requirement_id","certification_type_id","user_id","signed_off_by","signed_off_at","note","created_at"]},"CreateCertificationSignOffRequest":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid","description":"`users.id` of the user being signed off."},"signed_off_at":{"type":"string","format":"date-time","description":"Omit for now; set to backdate a sign-off."},"note":{"type":"string","maxLength":2000}},"required":["user_id"]},"CertificationSignOffList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CertificationSignOff"}}},"required":["items"]},"CertificationTypeList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CertificationType"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"CertificationType":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the certification type."},"subject_matter_id":{"type":"string","format":"uuid","description":"The subject matter whose records and evidence satisfy the rules."},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"description_markdown":{"type":["string","null"],"description":"Long-form narrative markdown below the summary line; never parsed."},"awarding_mode":{"type":"string","enum":["automatic","approval"],"description":"automatic: the user claims the certification themselves once every rule is satisfied. approval: the user requests it and a permission holder decides."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","owner_organization_id","owner_department_id","subject_matter_id","slug","name","description","description_markdown","awarding_mode","archived_at","created_at"]},"CreateCertificationTypeRequest":{"type":"object","properties":{"owner_organization_id":{"type":"string","format":"uuid","description":"Organization the certification type belongs to. Always required."},"owner_department_id":{"type":"string","format":"uuid","description":"Omit for a type owned by the organization directly."},"subject_matter_id":{"type":"string","format":"uuid","description":"A subject matter of the same organization; immutable once created."},"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"forklift-operator","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like forklift-operator."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Certified forklift operator"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."},"description_markdown":{"type":"string","maxLength":50000,"description":"Omit or send empty for no long-form markdown description."},"awarding_mode":{"type":"string","enum":["automatic","approval"],"description":"automatic: the user claims the certification themselves once every rule is satisfied. approval: the user requests it and a permission holder decides."}},"required":["owner_organization_id","subject_matter_id","slug","name","awarding_mode"]},"DescriptionUploadTicket":{"type":"object","properties":{"pathname":{"type":"string","description":"Where the private blob will live; pass it back when creating the attachment. Not itself a URL."},"upload_url":{"type":"string","description":"PUT the file bytes here, with the declared content type."},"expires_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["pathname","upload_url","expires_at"]},"CreateDescriptionUploadRequest":{"type":"object","properties":{"file_name":{"type":"string","minLength":1,"maxLength":300},"content_type":{"type":"string","minLength":1,"maxLength":200,"description":"The file's content type; any type is allowed, but the upload pins this one."},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":524288000}},"required":["file_name","content_type","size_bytes"]},"DescriptionAttachment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"file_name":{"type":"string","description":"The uploaded file's original name, kept verbatim for downloads."},"content_type":{"type":"string","description":"The content type the store recorded at upload; any type is allowed."},"label":{"type":["string","null"],"description":"Admin-written label shown in place of the file name, or null."},"position":{"type":"integer","description":"Authored order the attachments render in."}},"required":["id","file_name","content_type","label","position"]},"CreateDescriptionAttachmentRequest":{"type":"object","properties":{"pathname":{"type":"string","minLength":1,"description":"The pathname an upload ticket was issued for, after PUTting the file."},"file_name":{"type":"string","minLength":1,"maxLength":300},"label":{"type":"string","maxLength":200,"description":"Omit or send empty to show the file name instead."}},"required":["pathname","file_name"]},"UpdateDescriptionAttachmentRequest":{"type":"object","properties":{"label":{"type":["string","null"],"maxLength":200,"description":"Send null or empty to clear the label back to the file name."},"position":{"type":"integer","minimum":0,"description":"New slot in the authored order."}}},"CertificationTypeDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the certification type."},"subject_matter_id":{"type":"string","format":"uuid","description":"The subject matter whose records and evidence satisfy the rules."},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"description_markdown":{"type":["string","null"],"description":"Long-form narrative markdown below the summary line; never parsed."},"awarding_mode":{"type":"string","enum":["automatic","approval"],"description":"automatic: the user claims the certification themselves once every rule is satisfied. approval: the user requests it and a permission holder decides."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"rules":{"type":"array","items":{"$ref":"#/components/schemas/CertificationRule"},"description":"The proof rules of this type; all of them must be satisfied at once."},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DescriptionAttachment"},"description":"The type's description attachments, in authored order — e.g. a blank form to fill and submit as evidence. The bytes are fetched through GET /certifications/{cert_id}/attachments/{attachment_id}/download."}},"required":["id","owner_organization_id","owner_department_id","subject_matter_id","slug","name","description","description_markdown","awarding_mode","archived_at","created_at","rules","attachments"]},"CertificationRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"certification_type_id":{"type":"string","format":"uuid"},"rule_kind":{"type":"string","enum":["subject_hours","subject_record_count","evidence_document","manual_sign_off","quiz_pass","external_course_pass"],"description":"subject_hours: evidenced hours on the type's subject must reach threshold_hours. subject_record_count: completed records on the subject must reach threshold_count. evidence_document: a completed record on the subject must carry evidence of evidence_type. manual_sign_off: a permission holder must record a sign-off for the rule. quiz_pass: the user must have a passing attempt of the quiz named by quiz_id. external_course_pass: the user must have a passing grade of the external LMS course named by external_course_id."},"label":{"type":["string","null"]},"threshold_hours":{"type":["number","null"]},"threshold_count":{"type":["number","null"]},"evidence_type":{"type":["string","null"],"enum":["score","date_range","quantity","url","file","pass_fail","note",null]},"quiz_id":{"type":["string","null"],"format":"uuid"},"external_course_id":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","certification_type_id","rule_kind","label","threshold_hours","threshold_count","evidence_type","quiz_id","external_course_id","created_at"]},"UpdateCertificationTypeRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the description."},"description_markdown":{"type":["string","null"],"maxLength":50000,"description":"Send null to clear the long-form markdown description."},"awarding_mode":{"type":"string","enum":["automatic","approval"],"description":"Changing the mode does not touch existing requests: pending requests on a now-automatic type remain decidable and withdrawable."}}},"CreateCertificationRuleRequest":{"type":"object","properties":{"rule_kind":{"type":"string","enum":["subject_hours","subject_record_count","evidence_document","manual_sign_off","quiz_pass","external_course_pass"],"description":"subject_hours: evidenced hours on the type's subject must reach threshold_hours. subject_record_count: completed records on the subject must reach threshold_count. evidence_document: a completed record on the subject must carry evidence of evidence_type. manual_sign_off: a permission holder must record a sign-off for the rule. quiz_pass: the user must have a passing attempt of the quiz named by quiz_id. external_course_pass: the user must have a passing grade of the external LMS course named by external_course_id."},"label":{"type":"string","minLength":1,"maxLength":200,"description":"Human label shown wherever the rule's progress is, e.g. \"20 simulator hours\"."},"threshold_hours":{"type":"number","exclusiveMinimum":0,"description":"Hours required, for subject_hours rules. Counted from quantity evidence in hours or minutes, plus the elapsed time of date_range evidence.","example":20},"threshold_count":{"type":"integer","exclusiveMinimum":0,"description":"Completed records required, for subject_record_count rules.","example":3},"evidence_type":{"type":"string","enum":["score","date_range","quantity","url","file","pass_fail","note"],"description":"Evidence type that must be present, for evidence_document rules."},"quiz_id":{"type":"string","format":"uuid","description":"Quiz that must be passed, for quiz_pass rules. A quiz of the type's own organization."},"external_course_id":{"type":"string","format":"uuid","description":"Catalogue id of the external LMS course that must be passed, for external_course_pass rules. A course of the type's own organization. Passing is decided by the course: its passing_score when set, the imported passed flag otherwise."}},"required":["rule_kind"]},"UpdateCertificationRuleRequest":{"type":"object","properties":{"label":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"Send null to clear the label."},"threshold_hours":{"type":"number","exclusiveMinimum":0,"description":"Hours required, for subject_hours rules. Counted from quantity evidence in hours or minutes, plus the elapsed time of date_range evidence.","example":20},"threshold_count":{"type":"integer","exclusiveMinimum":0,"description":"Completed records required, for subject_record_count rules.","example":3},"evidence_type":{"type":"string","enum":["score","date_range","quantity","url","file","pass_fail","note"],"description":"Evidence type that must be present, for evidence_document rules."},"quiz_id":{"type":"string","format":"uuid","description":"Quiz that must be passed, for quiz_pass rules. A quiz of the type's own organization."},"external_course_id":{"type":"string","format":"uuid","description":"Catalogue id of the external LMS course that must be passed, for external_course_pass rules. A course of the type's own organization. Passing is decided by the course: its passing_score when set, the imported passed flag otherwise."}}},"RoleRequiredCertificationList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RoleRequiredCertification"}}},"required":["items"]},"RoleRequiredCertification":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"role_id":{"type":"string","format":"uuid"},"department_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"certification_type_id":{"type":"string","format":"uuid"},"recert_scheme":{"type":"string","enum":["none","anniversary","fixed_calendar"],"description":"none: certify once, never again. anniversary: due recert_interval_months after each award. fixed_calendar: due on recert_anchor_month/recert_anchor_day each year."},"recert_interval_months":{"type":["number","null"]},"recert_anchor_month":{"type":["number","null"]},"recert_anchor_day":{"type":["number","null"]},"initial_due_at":{"type":["string","null"],"format":"date-time","description":"When holders' *initial* certification falls due; null when the role sets no deadline. Per-person due dates override it."},"created_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"certification_type":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"awarding_mode":{"type":"string","enum":["automatic","approval"],"description":"automatic: the user claims the certification themselves once every rule is satisfied. approval: the user requests it and a permission holder decides."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."}},"required":["id","slug","name","awarding_mode","archived_at"],"description":"The linked certification type, for display."}},"required":["id","role_id","department_id","organization_id","certification_type_id","recert_scheme","recert_interval_months","recert_anchor_month","recert_anchor_day","initial_due_at","created_by","created_at","certification_type"]},"CreateRoleRequiredCertificationRequest":{"type":"object","properties":{"certification_type_id":{"type":"string","format":"uuid","description":"A certification type of the same organization as the role's department."},"recert_scheme":{"type":"string","enum":["none","anniversary","fixed_calendar"],"description":"none: certify once, never again. anniversary: due recert_interval_months after each award. fixed_calendar: due on recert_anchor_month/recert_anchor_day each year."},"recert_interval_months":{"type":"integer","exclusiveMinimum":0,"description":"Months between recertifications, for the anniversary scheme.","example":12},"recert_anchor_month":{"type":"integer","minimum":1,"maximum":12,"description":"Calendar month (1-12) the certification falls due, for fixed_calendar.","example":3},"recert_anchor_day":{"type":"integer","minimum":1,"maximum":31,"description":"Day of the anchor month (1-31, clamped to the month's length), for fixed_calendar.","example":31},"initial_due_at":{"type":"string","format":"date-time","description":"When the *initial* certification falls due for every holder of the role. A member's per-person due date overrides it; omit to set no role-level deadline."}},"required":["certification_type_id","recert_scheme"]},"UpdateRoleRequiredCertificationRequest":{"type":"object","properties":{"recert_scheme":{"type":"string","enum":["none","anniversary","fixed_calendar"],"description":"none: certify once, never again. anniversary: due recert_interval_months after each award. fixed_calendar: due on recert_anchor_month/recert_anchor_day each year."},"recert_interval_months":{"type":"integer","exclusiveMinimum":0,"description":"Months between recertifications, for the anniversary scheme.","example":12},"recert_anchor_month":{"type":"integer","minimum":1,"maximum":12,"description":"Calendar month (1-12) the certification falls due, for fixed_calendar.","example":3},"recert_anchor_day":{"type":"integer","minimum":1,"maximum":31,"description":"Day of the anchor month (1-31, clamped to the month's length), for fixed_calendar.","example":31},"initial_due_at":{"type":"string","format":"date-time","description":"When the *initial* certification falls due for every holder of the role. A member's per-person due date overrides it; omit to set no role-level deadline."}},"required":["recert_scheme"]},"RoleCertificationCompletion":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/TeamRoleRef"},"trainee_count":{"type":"integer","description":"Holders of the role whose grant has not expired."},"fully_complete_count":{"type":"integer","description":"Trainees whose every required type is complete."},"fully_complete_percent":{"type":["number","null"],"description":"fully_complete_count over trainee_count, 0–100; null with no trainees."},"certifications":{"type":"array","items":{"$ref":"#/components/schemas/RoleRequiredCertificationStat"},"description":"One tally per required certification type, archived types left out. Empty when the role requires nothing — every trainee is then vacuously complete."},"trainees":{"type":"array","items":{"$ref":"#/components/schemas/TraineeCompletion"},"description":"The roster, named members first, each with their per-type standing."}},"required":["role","trainee_count","fully_complete_count","fully_complete_percent","certifications","trainees"]},"TeamRoleRef":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid"},"role_slug":{"type":"string"},"role_name":{"type":"string"},"department_id":{"type":"string","format":"uuid"},"department_slug":{"type":"string"},"department_name":{"type":"string"},"organization_id":{"type":"string","format":"uuid"},"organization_slug":{"type":"string"},"organization_name":{"type":"string"}},"required":["role_id","role_slug","role_name","department_id","department_slug","department_name","organization_id","organization_slug","organization_name"]},"RoleRequiredCertificationStat":{"type":"object","properties":{"certification_type":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"awarding_mode":{"type":"string","enum":["automatic","approval"]}},"required":["id","slug","name","awarding_mode"]},"complete_count":{"type":"integer"},"overdue_count":{"type":"integer"},"not_certified_count":{"type":"integer"},"complete_percent":{"type":["number","null"],"description":"complete_count over the trainee count, 0–100; null with no trainees."}},"required":["certification_type","complete_count","overdue_count","not_certified_count","complete_percent"]},"TraineeCompletion":{"type":"object","properties":{"user_id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"],"description":"The trainee's profile name from users; null when unset."},"email":{"type":["string","null"]},"fully_complete":{"type":"boolean","description":"Every required type of the role is complete; vacuously true with none."},"requirements":{"type":"array","items":{"$ref":"#/components/schemas/TraineeRequirementStatus"},"description":"In the same order as the completion's certifications."}},"required":["user_id","display_name","email","fully_complete","requirements"]},"TraineeRequirementStatus":{"type":"object","properties":{"certification_type_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["complete","overdue","not_certified"],"description":"complete: current award and not past due. overdue: past due, lapsed or never certified. not_certified: no award, no due date passed."},"due_at":{"type":["string","null"],"format":"date-time","description":"When the certification (or next recertification) falls due, if a date applies."},"awarded_at":{"type":["string","null"],"format":"date-time","description":"When the current award was made; null while uncertified."}},"required":["certification_type_id","state","due_at","awarded_at"]},"TeamCertificationCompletionList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RoleCertificationCompletion"},"description":"Every role in the caller's managed subtree that requires at least one live certification type, fully tallied."}},"required":["items"]},"QuizQuestionList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/QuizQuestion"}}},"required":["items"]},"QuizQuestion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"question_bank_id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["true_false","multiple_choice","multi_select","ordering","short_answer","numeric","matching"],"description":"true_false: answered true or false. multiple_choice: exactly one correct option. multi_select: several options may be correct, scored all-or-nothing. ordering: the options are items whose authored order is the answer. short_answer: the options are the accepted spellings of a typed answer. numeric: a number within a tolerance of correct_number. matching: each option is a label/match_label pair the taker reunites."},"prompt":{"type":"string"},"prompt_format":{"type":"string","enum":["plaintext","markdown","pdf"],"description":"plaintext: the prompt renders verbatim. markdown: the same prompt text renders as Markdown. pdf: an uploaded PDF carries the question; the prompt text is the instruction shown above it."},"prompt_pdf_file_name":{"type":["string","null"],"description":"Original name of the attached PDF, for pdf prompts; null otherwise. The file itself is fetched through the question's pdf endpoint."},"explanation":{"type":["string","null"],"description":"Shown in answer review when the quiz reveals correct answers."},"correct_boolean":{"type":["boolean","null"],"description":"The answer of a true_false question; null for every other kind."},"correct_number":{"type":["number","null"],"description":"The answer of a numeric question; null for every other kind."},"tolerance":{"type":["number","null"],"description":"How far a numeric response may stray and still count; null otherwise."},"position":{"type":"integer"},"options":{"type":"array","items":{"$ref":"#/components/schemas/QuizQuestionOption"},"description":"Choices, ordering items (authored order = correct order), accepted short answers or matching pairs, in authored order; empty for true_false and numeric questions."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","question_bank_id","kind","prompt","prompt_format","prompt_pdf_file_name","explanation","correct_boolean","correct_number","tolerance","position","options","archived_at","created_at"]},"QuizQuestionOption":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"is_correct":{"type":"boolean","description":"Meaningful on choice questions; always true on accepted short answers and false on ordering items and matching pairs."},"match_label":{"type":["string","null"],"description":"The right-hand side of a matching pair; null on every other kind."},"position":{"type":"integer"}},"required":["id","label","is_correct","match_label","position"]},"CreateQuizQuestionRequest":{"type":"object","properties":{"kind":{"type":"string","enum":["true_false","multiple_choice","multi_select","ordering","short_answer","numeric","matching"],"description":"true_false: answered true or false. multiple_choice: exactly one correct option. multi_select: several options may be correct, scored all-or-nothing. ordering: the options are items whose authored order is the answer. short_answer: the options are the accepted spellings of a typed answer. numeric: a number within a tolerance of correct_number. matching: each option is a label/match_label pair the taker reunites."},"prompt":{"type":"string","minLength":1,"maxLength":2000,"example":"Fire doors may be propped open during deliveries."},"prompt_format":{"type":"string","enum":["plaintext","markdown","pdf"],"description":"Defaults to plaintext."},"prompt_pdf":{"$ref":"#/components/schemas/QuestionPromptPdf"},"explanation":{"type":"string","maxLength":2000,"description":"Omit or send empty for no explanation."},"correct_boolean":{"type":"boolean","description":"Required for true_false questions; omit for every other kind."},"correct_number":{"type":"number","description":"numeric questions only.","example":42},"tolerance":{"type":"number","minimum":0,"description":"numeric questions only; 0 demands the exact value.","example":0.5},"options":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":500},"is_correct":{"type":"boolean","description":"Choice kinds only; ignored (and stored per kind) on the others."},"match_label":{"type":"string","minLength":1,"maxLength":500,"description":"Required on every option of a matching question; omit elsewhere."}},"required":["label"]},"minItems":1,"maxItems":10,"description":"Up to 10 options in presentation order — at least two, or one accepted answer for short_answer. multiple_choice needs exactly one correct option; multi_select at least one; ordering lists items in their correct order; matching gives each option its match_label."}},"required":["kind","prompt"]},"QuestionPromptPdf":{"type":"object","properties":{"pathname":{"type":"string","minLength":1,"description":"The pathname issued by the bank's question-pdfs endpoint, after the PUT."},"file_name":{"type":"string","minLength":1,"maxLength":300}},"required":["pathname","file_name"],"description":"Required for pdf prompts; omit for plaintext and markdown."},"QuestionPdfUploadTicket":{"type":"object","properties":{"pathname":{"type":"string","description":"Where the private blob will live; pass it as prompt_pdf.pathname when creating or updating the question. Not itself a URL."},"upload_url":{"type":"string","description":"PUT the file bytes here, with content type application/pdf."},"expires_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["pathname","upload_url","expires_at"]},"CreateQuestionPdfUploadRequest":{"type":"object","properties":{"file_name":{"type":"string","minLength":1,"maxLength":300},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":26214400}},"required":["file_name","size_bytes"]},"QuizQuestionBankList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/QuizQuestionBank"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"QuizQuestionBank":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the bank."},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"question_count":{"type":"integer","description":"Unarchived questions currently in the bank."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","owner_organization_id","owner_department_id","slug","name","description","question_count","archived_at","created_at"]},"CreateQuizQuestionBankRequest":{"type":"object","properties":{"owner_organization_id":{"type":"string","format":"uuid","description":"Organization the bank belongs to. Always required."},"owner_department_id":{"type":"string","format":"uuid","description":"Omit for a bank owned by the organization directly."},"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"fire-safety-basics","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like fire-safety-basics."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Fire safety basics"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."}},"required":["owner_organization_id","slug","name"]},"UpdateQuizQuestionBankRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the description."}}},"UpdateQuizQuestionRequest":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":2000},"prompt_format":{"type":"string","enum":["plaintext","markdown","pdf"],"description":"Switching between plaintext and markdown re-renders the same prompt text. Switching to pdf needs prompt_pdf (unless one is already attached); switching away discards the attachment."},"prompt_pdf":{"allOf":[{"$ref":"#/components/schemas/QuestionPromptPdf"},{"description":"Attaches (or replaces) the question's PDF; pdf prompts only."}]},"explanation":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the explanation."},"correct_boolean":{"type":"boolean","description":"For true_false questions only."},"correct_number":{"type":"number","description":"numeric questions only.","example":42},"tolerance":{"type":"number","minimum":0,"description":"numeric questions only; 0 demands the exact value.","example":0.5},"options":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":500},"is_correct":{"type":"boolean","description":"Choice kinds only; ignored (and stored per kind) on the others."},"match_label":{"type":"string","minLength":1,"maxLength":500,"description":"Required on every option of a matching question; omit elsewhere."}},"required":["label"]},"minItems":1,"maxItems":10,"description":"For questions with options only. Replaces the whole option set; answer review of attempts that saw the old options degrades accordingly. Grading of submitted attempts never changes."}}},"AssignedQuizList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AssignedQuiz"}}},"required":["items"]},"AssignedQuiz":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"organization_id":{"type":"string","format":"uuid"},"organization_name":{"type":"string"},"delivery_kind":{"type":"string","enum":["live","async"],"description":"live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply."},"time_limit_seconds":{"type":["integer","null"]},"pass_percentage":{"type":"number"},"max_attempts":{"type":["integer","null"]},"show_correct_answers":{"type":"boolean"},"current_sitting":{"$ref":"#/components/schemas/ScheduledQuiz"},"next_sitting":{"allOf":[{"$ref":"#/components/schemas/ScheduledQuiz"},{"description":"The next sitting yet to open, for live quizzes; null otherwise."}]},"state":{"type":"string","enum":["upcoming","open","closed"],"description":"Takeability right now: async published quizzes are open; live ones are open inside a sitting, upcoming while one is yet to open, closed otherwise."},"attempts_used":{"type":"integer","description":"Attempts counted against max_attempts: within the current sitting for live quizzes (0 when none is open), lifetime for async ones."},"in_progress_attempt_id":{"type":["string","null"],"format":"uuid"},"latest_attempt":{"$ref":"#/components/schemas/QuizAttempt"},"has_passed":{"type":["boolean","null"],"description":"Whether any attempt passed; null while no attempt has been submitted or results are withheld."}},"required":["id","slug","name","description","organization_id","organization_name","delivery_kind","time_limit_seconds","pass_percentage","max_attempts","show_correct_answers","current_sitting","next_sitting","state","attempts_used","in_progress_attempt_id","latest_attempt","has_passed"]},"ScheduledQuiz":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"quiz_id":{"type":"string","format":"uuid"},"label":{"type":["string","null"],"description":"Human label for the sitting, e.g. \"2026 annual recertification\"."},"opens_at":{"type":"string","format":"date-time"},"closes_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","quiz_id","label","opens_at","closes_at","created_at"],"description":"The sitting open right now, for live quizzes; null otherwise."},"QuizAttempt":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"quiz_id":{"type":"string","format":"uuid"},"scheduled_quiz_id":{"type":["string","null"],"format":"uuid","description":"The sitting a live attempt was sat in; null on async attempts."},"user_id":{"type":"string","format":"uuid"},"user_display_name":{"type":["string","null"],"description":"Display name of the taker's user account, when the read joins it; else null."},"attempt_number":{"type":"integer"},"status":{"type":"string","enum":["in_progress","submitted"]},"started_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"deadline_at":{"type":["string","null"],"format":"date-time","description":"When the attempt auto-submits as-is: started_at plus the time limit, capped by the sitting's close. Null when neither applies."},"submitted_at":{"type":["string","null"],"format":"date-time"},"auto_submitted":{"type":"boolean","description":"True when the deadline finalized the attempt rather than the taker."},"question_count":{"type":"integer"},"correct_count":{"type":["integer","null"]},"score_percentage":{"type":["number","null"]},"passed":{"type":["boolean","null"]},"results_visible":{"type":"boolean","description":"Whether the caller may see this attempt's results. False nulls the three fields above."}},"required":["id","quiz_id","scheduled_quiz_id","user_id","user_display_name","attempt_number","status","started_at","deadline_at","submitted_at","auto_submitted","question_count","correct_count","score_percentage","passed","results_visible"]},"QuizAttemptDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"quiz_id":{"type":"string","format":"uuid"},"scheduled_quiz_id":{"type":["string","null"],"format":"uuid","description":"The sitting a live attempt was sat in; null on async attempts."},"user_id":{"type":"string","format":"uuid"},"user_display_name":{"type":["string","null"],"description":"Display name of the taker's user account, when the read joins it; else null."},"attempt_number":{"type":"integer"},"status":{"type":"string","enum":["in_progress","submitted"]},"started_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"deadline_at":{"type":["string","null"],"format":"date-time","description":"When the attempt auto-submits as-is: started_at plus the time limit, capped by the sitting's close. Null when neither applies."},"submitted_at":{"type":["string","null"],"format":"date-time"},"auto_submitted":{"type":"boolean","description":"True when the deadline finalized the attempt rather than the taker."},"question_count":{"type":"integer"},"correct_count":{"type":["integer","null"]},"score_percentage":{"type":["number","null"]},"passed":{"type":["boolean","null"]},"results_visible":{"type":"boolean","description":"Whether the caller may see this attempt's results. False nulls the three fields above."},"questions":{"type":"array","items":{"$ref":"#/components/schemas/TakerQuestion"},"description":"The attempt's paper in presentation order, answers stripped."}},"required":["id","quiz_id","scheduled_quiz_id","user_id","user_display_name","attempt_number","status","started_at","deadline_at","submitted_at","auto_submitted","question_count","correct_count","score_percentage","passed","results_visible","questions"]},"TakerQuestion":{"type":"object","properties":{"question_id":{"type":"string","format":"uuid"},"position":{"type":"integer"},"kind":{"type":"string","enum":["true_false","multiple_choice","multi_select","ordering","short_answer","numeric","matching"],"description":"true_false: answered true or false. multiple_choice: exactly one correct option. multi_select: several options may be correct, scored all-or-nothing. ordering: the options are items whose authored order is the answer. short_answer: the options are the accepted spellings of a typed answer. numeric: a number within a tolerance of correct_number. matching: each option is a label/match_label pair the taker reunites."},"prompt":{"type":"string"},"prompt_format":{"type":"string","enum":["plaintext","markdown","pdf"],"description":"plaintext: the prompt renders verbatim. markdown: the same prompt text renders as Markdown. pdf: an uploaded PDF carries the question; the prompt text is the instruction shown above it."},"prompt_pdf_file_name":{"type":["string","null"],"description":"Original name of the attached PDF, for pdf prompts; null otherwise. Fetch the file through the attempt's question pdf endpoint."},"options":{"type":"array","items":{"$ref":"#/components/schemas/TakerQuestionOption"},"description":"Choices, ordering items or the left column of a matching question, in the order this attempt presents them — correctness stripped. Empty for true_false, short_answer and numeric questions."},"match_options":{"type":"array","items":{"$ref":"#/components/schemas/TakerQuestionOption"},"description":"The right column of a matching question in its own shuffled order, each entry identified by the option whose match_label it is. Empty for every other kind."},"response_boolean":{"type":["boolean","null"]},"selected_option_ids":{"type":["array","null"],"items":{"type":"string","format":"uuid"},"description":"Chosen options of a choice question, or every item of an ordering question in the taker's arrangement."},"response_text":{"type":["string","null"],"description":"A short_answer response."},"response_number":{"type":["number","null"],"description":"A numeric response."},"response_matches":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ResponseMatch"},"description":"The pairs made so far on a matching question."},"answered_at":{"type":["string","null"],"format":"date-time"}},"required":["question_id","position","kind","prompt","prompt_format","prompt_pdf_file_name","options","match_options","response_boolean","selected_option_ids","response_text","response_number","response_matches","answered_at"]},"TakerQuestionOption":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"}},"required":["id","label"]},"ResponseMatch":{"type":"object","properties":{"option_id":{"type":"string","format":"uuid","description":"An entry of the question's options."},"match_option_id":{"type":"string","format":"uuid","description":"An entry of the question's match_options — the right-hand label chosen."}},"required":["option_id","match_option_id"]},"QuizAttemptReview":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"quiz_id":{"type":"string","format":"uuid"},"scheduled_quiz_id":{"type":["string","null"],"format":"uuid","description":"The sitting a live attempt was sat in; null on async attempts."},"user_id":{"type":"string","format":"uuid"},"user_display_name":{"type":["string","null"],"description":"Display name of the taker's user account, when the read joins it; else null."},"attempt_number":{"type":"integer"},"status":{"type":"string","enum":["in_progress","submitted"]},"started_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"deadline_at":{"type":["string","null"],"format":"date-time","description":"When the attempt auto-submits as-is: started_at plus the time limit, capped by the sitting's close. Null when neither applies."},"submitted_at":{"type":["string","null"],"format":"date-time"},"auto_submitted":{"type":"boolean","description":"True when the deadline finalized the attempt rather than the taker."},"question_count":{"type":"integer"},"correct_count":{"type":["integer","null"]},"score_percentage":{"type":["number","null"]},"passed":{"type":["boolean","null"]},"results_visible":{"type":"boolean","description":"Whether the caller may see this attempt's results. False nulls the three fields above."},"questions":{"type":"array","items":{"$ref":"#/components/schemas/QuizReviewQuestion"}}},"required":["id","quiz_id","scheduled_quiz_id","user_id","user_display_name","attempt_number","status","started_at","deadline_at","submitted_at","auto_submitted","question_count","correct_count","score_percentage","passed","results_visible","questions"]},"QuizReviewQuestion":{"type":"object","properties":{"question_id":{"type":"string","format":"uuid"},"position":{"type":"integer"},"kind":{"type":"string","enum":["true_false","multiple_choice","multi_select","ordering","short_answer","numeric","matching"],"description":"true_false: answered true or false. multiple_choice: exactly one correct option. multi_select: several options may be correct, scored all-or-nothing. ordering: the options are items whose authored order is the answer. short_answer: the options are the accepted spellings of a typed answer. numeric: a number within a tolerance of correct_number. matching: each option is a label/match_label pair the taker reunites."},"prompt":{"type":"string"},"prompt_format":{"type":"string","enum":["plaintext","markdown","pdf"],"description":"plaintext: the prompt renders verbatim. markdown: the same prompt text renders as Markdown. pdf: an uploaded PDF carries the question; the prompt text is the instruction shown above it."},"prompt_pdf_file_name":{"type":["string","null"]},"explanation":{"type":["string","null"]},"correct_boolean":{"type":["boolean","null"]},"correct_number":{"type":["number","null"]},"tolerance":{"type":["number","null"]},"options":{"type":"array","items":{"$ref":"#/components/schemas/ReviewQuestionOption"},"description":"In presentation order for choice and matching questions; in *authored* (correct) order for ordering questions; the accepted answers for short_answer."},"response_boolean":{"type":["boolean","null"]},"selected_option_ids":{"type":["array","null"],"items":{"type":"string","format":"uuid"},"description":"Chosen options of a choice question, or every item of an ordering question in the taker's arrangement."},"response_text":{"type":["string","null"],"description":"A short_answer response."},"response_number":{"type":["number","null"],"description":"A numeric response."},"response_matches":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ResponseMatch"},"description":"The pairs made so far on a matching question."},"is_correct":{"type":["boolean","null"],"description":"As graded at submit time; unanswered questions were graded incorrect."}},"required":["question_id","position","kind","prompt","prompt_format","prompt_pdf_file_name","explanation","correct_boolean","correct_number","tolerance","options","response_boolean","selected_option_ids","response_text","response_number","response_matches","is_correct"]},"ReviewQuestionOption":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":"string"},"is_correct":{"type":"boolean"},"match_label":{"type":["string","null"]}},"required":["id","label","is_correct","match_label"]},"AnswerQuizQuestionRequest":{"type":"object","properties":{"question_id":{"type":"string","format":"uuid"},"response_boolean":{"type":"boolean"},"selected_option_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":20,"description":"multiple_choice: exactly one id. multi_select: the chosen ids. ordering: every presented item id, in the taker's arrangement."},"response_text":{"type":"string","minLength":1,"maxLength":500},"response_number":{"type":"number"},"response_matches":{"type":"array","items":{"$ref":"#/components/schemas/ResponseMatch"},"minItems":1,"maxItems":10}},"required":["question_id"]},"QuizAttemptList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/QuizAttempt"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"CreateScheduledQuizRequest":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"Omit or send empty for an unlabelled sitting.","example":"2026 annual recertification"},"opens_at":{"type":"string","format":"date-time"},"closes_at":{"type":"string","format":"date-time"}},"required":["opens_at","closes_at"]},"UpdateScheduledQuizRequest":{"type":"object","properties":{"label":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"Send null to clear the label."},"opens_at":{"type":"string","format":"date-time"},"closes_at":{"type":"string","format":"date-time"}}},"QuizAssignmentList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/QuizAssignment"}}},"required":["items"]},"QuizAssignment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"quiz_id":{"type":"string","format":"uuid"},"assignee_kind":{"type":"string","enum":["role","user"],"description":"role: everyone granted the role. user: one directory member."},"role_id":{"type":["string","null"],"format":"uuid"},"role_name":{"type":["string","null"]},"department_id":{"type":["string","null"],"format":"uuid"},"department_name":{"type":["string","null"]},"user_id":{"type":["string","null"],"format":"uuid","description":"User id of an individually assigned member."},"user_display_name":{"type":["string","null"]},"user_email":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","quiz_id","assignee_kind","role_id","role_name","department_id","department_name","user_id","user_display_name","user_email","created_at"]},"CreateQuizAssignmentRequest":{"type":"object","properties":{"role_id":{"type":"string","format":"uuid","description":"Assign to every holder of this department role."},"user_id":{"type":"string","format":"uuid","description":"Assign to one member of the quiz's organization, by user id."}}},"QuizDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the quiz."},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["draft","published"],"description":"Draft quizzes are invisible to assignees; published ones are takeable."},"delivery_kind":{"type":"string","enum":["live","async"],"description":"live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply."},"time_limit_seconds":{"type":["integer","null"]},"selection_mode":{"type":"string","enum":["fixed","shuffle","random_draw"],"description":"fixed: every question in authored order, the same for everyone. shuffle: every question, order and options shuffled per attempt. random_draw: each bank link contributes draw_count randomly chosen questions (all when null), shuffled."},"pass_percentage":{"type":"number"},"max_attempts":{"type":["integer","null"],"description":"Null means unlimited attempts."},"show_results_immediately":{"type":"boolean","description":"Whether a taker sees score and pass/fail right after submitting."},"show_correct_answers":{"type":"boolean","description":"Whether answer review (your answer vs. correct) is ever shown to takers."},"results_released_at":{"type":["string","null"],"format":"date-time","description":"Manual release stamp for held-back results. An attempt's results are visible to its taker when show_results_immediately, once the sitting it was sat in has closed, or once this is set."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"bank_links":{"type":"array","items":{"$ref":"#/components/schemas/QuizBankLink"},"description":"The banks this quiz draws from, in presentation order."},"subjects":{"type":"array","items":{"$ref":"#/components/schemas/QuizSubject"},"description":"Subject matters the per-taker learning record is filed under."},"schedules":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledQuiz"},"description":"The quiz's scheduled sittings in calendar order. A live quiz is takeable only while one of them is open; async quizzes ignore them."}},"required":["id","owner_organization_id","owner_department_id","slug","name","description","status","delivery_kind","time_limit_seconds","selection_mode","pass_percentage","max_attempts","show_results_immediately","show_correct_answers","results_released_at","archived_at","created_at","bank_links","subjects","schedules"]},"QuizBankLink":{"type":"object","properties":{"question_bank_id":{"type":"string","format":"uuid"},"bank_slug":{"type":"string"},"bank_name":{"type":"string"},"position":{"type":"integer"},"draw_count":{"type":["integer","null"],"description":"Questions a random_draw quiz pulls from this bank; null = all."},"question_count":{"type":"integer","description":"Unarchived questions currently in the linked bank."}},"required":["question_bank_id","bank_slug","bank_name","position","draw_count","question_count"]},"QuizSubject":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"SetQuizBankLinksRequest":{"type":"object","properties":{"links":{"type":"array","items":{"type":"object","properties":{"question_bank_id":{"type":"string","format":"uuid"},"draw_count":{"type":"integer","exclusiveMinimum":0,"description":"Questions a random_draw quiz pulls from this bank; omit for all."}},"required":["question_bank_id"]},"maxItems":50,"description":"Replaces the quiz's bank links; array order is presentation order."}},"required":["links"]},"SetQuizSubjectsRequest":{"type":"object","properties":{"subject_matter_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":50,"description":"Replaces the quiz's subject-matter tags."}},"required":["subject_matter_ids"]},"Quiz":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the quiz."},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["draft","published"],"description":"Draft quizzes are invisible to assignees; published ones are takeable."},"delivery_kind":{"type":"string","enum":["live","async"],"description":"live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply."},"time_limit_seconds":{"type":["integer","null"]},"selection_mode":{"type":"string","enum":["fixed","shuffle","random_draw"],"description":"fixed: every question in authored order, the same for everyone. shuffle: every question, order and options shuffled per attempt. random_draw: each bank link contributes draw_count randomly chosen questions (all when null), shuffled."},"pass_percentage":{"type":"number"},"max_attempts":{"type":["integer","null"],"description":"Null means unlimited attempts."},"show_results_immediately":{"type":"boolean","description":"Whether a taker sees score and pass/fail right after submitting."},"show_correct_answers":{"type":"boolean","description":"Whether answer review (your answer vs. correct) is ever shown to takers."},"results_released_at":{"type":["string","null"],"format":"date-time","description":"Manual release stamp for held-back results. An attempt's results are visible to its taker when show_results_immediately, once the sitting it was sat in has closed, or once this is set."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","owner_organization_id","owner_department_id","slug","name","description","status","delivery_kind","time_limit_seconds","selection_mode","pass_percentage","max_attempts","show_results_immediately","show_correct_answers","results_released_at","archived_at","created_at"]},"QuizList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Quiz"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"CreateQuizRequest":{"type":"object","properties":{"owner_organization_id":{"type":"string","format":"uuid","description":"Organization the quiz belongs to. Always required."},"owner_department_id":{"type":"string","format":"uuid","description":"Omit for a quiz owned by the organization directly."},"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"annual-fire-safety","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like fire-safety-basics."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Annual fire safety quiz"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."},"delivery_kind":{"type":"string","enum":["live","async"],"description":"live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply."},"time_limit_seconds":{"type":"integer","exclusiveMinimum":0,"description":"Per-attempt time limit in seconds; a live sitting's close still caps it.","example":1200},"selection_mode":{"type":"string","enum":["fixed","shuffle","random_draw"],"description":"fixed: every question in authored order, the same for everyone. shuffle: every question, order and options shuffled per attempt. random_draw: each bank link contributes draw_count randomly chosen questions (all when null), shuffled."},"pass_percentage":{"type":"number","minimum":0,"maximum":100,"description":"Score required to pass, as a percentage of the questions presented.","example":80},"max_attempts":{"type":"integer","exclusiveMinimum":0,"description":"Omit for unlimited attempts.","example":3},"show_results_immediately":{"type":"boolean","description":"Defaults to true."},"show_correct_answers":{"type":"boolean","description":"Defaults to false."},"bank_links":{"type":"array","items":{"type":"object","properties":{"question_bank_id":{"type":"string","format":"uuid"},"draw_count":{"type":"integer","exclusiveMinimum":0,"description":"Questions a random_draw quiz pulls from this bank; omit for all."}},"required":["question_bank_id"]},"maxItems":50,"description":"Question banks to link from the start, in presentation order; each must belong to the owning organization. Omit to link banks later via PUT /quizzes/{quiz_id}/banks."}},"required":["owner_organization_id","slug","name","delivery_kind","selection_mode","pass_percentage"]},"UpdateQuizRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the description."},"delivery_kind":{"type":"string","enum":["live","async"],"description":"live: takeable only while one of the quiz's scheduled sittings (scheduled_quizzes) is open — the same quiz can be scheduled year after year. async: self-scheduled, takeable whenever the quiz is published; sittings do not apply."},"time_limit_seconds":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Send null to remove the time limit.","example":1200},"selection_mode":{"type":"string","enum":["fixed","shuffle","random_draw"],"description":"fixed: every question in authored order, the same for everyone. shuffle: every question, order and options shuffled per attempt. random_draw: each bank link contributes draw_count randomly chosen questions (all when null), shuffled."},"pass_percentage":{"type":"number","minimum":0,"maximum":100,"description":"Score required to pass, as a percentage of the questions presented.","example":80},"max_attempts":{"type":["integer","null"],"exclusiveMinimum":0,"description":"Send null for unlimited attempts.","example":3},"show_results_immediately":{"type":"boolean"},"show_correct_answers":{"type":"boolean"}}},"MyJourneys":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/JourneyProgress"},"description":"Every unarchived journey assigned to the caller, with derived progress."}},"required":["items"]},"JourneyProgress":{"type":"object","properties":{"journey_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description_markdown":{"type":["string","null"]},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DescriptionAttachment"},"description":"The journey's description attachments, in authored order."},"levels":{"type":"array","items":{"$ref":"#/components/schemas/JourneyLevelProgress"},"description":"Every level in authored order, each with its derived state."},"current_level_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"The unlocked-but-incomplete levels — where the viewer is right now."},"complete":{"type":"boolean","description":"Whether every level is complete for the viewer."}},"required":["journey_id","slug","name","description_markdown","attachments","levels","current_level_ids","complete"]},"JourneyLevelProgress":{"type":"object","properties":{"level_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"description_markdown":{"type":["string","null"]},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DescriptionAttachment"},"description":"The level's description attachments, in authored order."},"position":{"type":"integer"},"depends_on_level_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"state":{"type":"string","enum":["locked","unlocked","complete"],"description":"complete: every applicable certification is awarded (vacuously complete when none applies). unlocked: every prerequisite level is complete. locked: rendered collapsed — presentation only, nothing is enforced server-side."},"applicable_count":{"type":"integer","description":"Certifications in the level that apply to the viewer."},"awarded_count":{"type":"integer","description":"Of the applicable ones, how many the viewer currently holds."},"has_overdue":{"type":"boolean","description":"Whether any applicable certification in the level is past due — the warning badge on an otherwise complete level. Never re-locks anything."},"certifications":{"type":"array","items":{"$ref":"#/components/schemas/JourneyCertProgress"}}},"required":["level_id","name","description","description_markdown","attachments","position","depends_on_level_ids","state","applicable_count","awarded_count","has_overdue","certifications"]},"JourneyCertProgress":{"type":"object","properties":{"certification_type_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"applies_to_user":{"type":"boolean","description":"Whether one of the viewer's unexpired role grants requires this type. Only applicable certifications count towards level completion."},"awarded":{"type":"boolean","description":"Whether the viewer currently holds a non-revoked award of this type."},"overdue":{"type":"boolean","description":"Whether the requirement is past due for the viewer (initial or recertification). Always false on certifications that do not apply — a warning badge, never a lock."}},"required":["certification_type_id","slug","name","applies_to_user","awarded","overdue"]},"JourneyLevel":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"journey_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"description_markdown":{"type":["string","null"],"description":"Long-form narrative markdown below the summary line; never parsed."},"position":{"type":"integer","description":"Authored order — the stable tie-break parallel branches render in."},"depends_on_level_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Levels that must all be complete before this one unlocks; empty for a root level."},"certifications":{"type":"array","items":{"$ref":"#/components/schemas/JourneyLevelCertification"},"description":"The certification types this level groups, in authored order."},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DescriptionAttachment"},"description":"The level's description attachments, in authored order. The bytes are fetched through GET /journey-levels/{level_id}/attachments/{attachment_id}/download."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","journey_id","name","description","description_markdown","position","depends_on_level_ids","certifications","attachments","created_at"]},"JourneyLevelCertification":{"type":"object","properties":{"certification_type_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"archived":{"type":"boolean","description":"Whether the certification type itself has been archived since placement."}},"required":["certification_type_id","slug","name","archived"]},"CreateJourneyLevelRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"example":"Level 1: Foundations"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."},"description_markdown":{"type":"string","maxLength":50000,"description":"Omit or send empty for no long-form markdown description."},"depends_on_level_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":50,"description":"Existing levels of the same journey this one depends on. The edges must keep the journey acyclic. Omit for a root level."},"certification_type_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":100,"description":"Certification types of the same organization to place in this level, in order. A type may appear in at most one level per journey."}},"required":["name"]},"UpdateJourneyLevelRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the description."},"description_markdown":{"type":["string","null"],"maxLength":50000,"description":"Send null to clear the long-form markdown description."},"position":{"type":"integer","minimum":0},"depends_on_level_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":50,"description":"Replaces the level's whole prerequisite set; must stay acyclic."},"certification_type_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":100,"description":"Replaces the level's whole certification list, in the given order."}}},"JourneyAssignment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"journey_id":{"type":"string","format":"uuid"},"assignee_kind":{"type":"string","enum":["role","department","organization"]},"role_id":{"type":["string","null"],"format":"uuid"},"role_name":{"type":["string","null"]},"department_id":{"type":["string","null"],"format":"uuid","description":"The role's department on a role assignment, the audience itself on a department one (covering departments nested beneath it too), null on an organization one."},"department_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","journey_id","assignee_kind","role_id","role_name","department_id","department_name","created_at"]},"CreateJourneyAssignmentRequest":{"type":"object","properties":{"assignee_kind":{"type":"string","enum":["role","department","organization"],"description":"role: holders of role_id. department: everyone with a role grant in department_id or any department nested beneath it. organization: every member."},"role_id":{"type":"string","format":"uuid","description":"Required for role assignments; must be a role of the journey's organization."},"department_id":{"type":"string","format":"uuid","description":"Required for department assignments; forbidden for organization ones."}},"required":["assignee_kind"]},"JourneyList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Journey"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"Journey":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"owner_organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Null when the organization itself owns the journey."},"slug":{"type":"string"},"name":{"type":"string"},"description_markdown":{"type":["string","null"],"description":"Narrative markdown rendered on the journey page; never parsed for structure."},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","owner_organization_id","owner_department_id","slug","name","description_markdown","archived_at","created_at"]},"CreateJourneyRequest":{"type":"object","properties":{"owner_organization_id":{"type":"string","format":"uuid","description":"Organization the journey belongs to. Always required."},"owner_department_id":{"type":"string","format":"uuid","description":"Omit for a journey owned by the organization directly."},"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"new-hire-path","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like new-hire-path."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"New hire path"},"description_markdown":{"type":"string","maxLength":50000,"description":"Omit or send empty for no markdown description."}},"required":["owner_organization_id","slug","name"]},"JourneyDetail":{"type":"object","properties":{"journey":{"$ref":"#/components/schemas/Journey"},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/DescriptionAttachment"},"description":"The journey's description attachments, in authored order. The bytes are fetched through GET /journeys/{journey_id}/attachments/{attachment_id}/download."},"levels":{"type":"array","items":{"$ref":"#/components/schemas/JourneyLevel"},"description":"Every level with its prerequisites and contents, in authored order."},"assignments":{"type":"array","items":{"$ref":"#/components/schemas/JourneyAssignment"},"description":"Who the journey is presented to."}},"required":["journey","attachments","levels","assignments"]},"UpdateJourneyRequest":{"type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$"},"name":{"type":"string","minLength":1,"maxLength":200},"description_markdown":{"type":["string","null"],"maxLength":50000,"description":"Send null to clear the markdown description."}}},"ExternalLmsServerList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExternalLmsServer"}}},"required":["items"]},"ExternalLmsServer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"base_url":{"type":["string","null"],"description":"Where the server lives, for humans; the LRS never calls it."},"lms_kind":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."}},"required":["id","organization_id","slug","name","description","base_url","lms_kind","created_at","archived_at"]},"CreateExternalLmsServerRequest":{"type":"object","properties":{"organization_id":{"type":"string","format":"uuid","description":"Organization whose catalogue the server belongs to."},"slug":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","example":"moodle-eu","description":"Unique within the organization. Slugs are lowercase letters and digits in words separated by single hyphens, like moodle-eu."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Moodle (EU campus)"},"description":{"type":"string","maxLength":2000,"description":"Omit or send empty for no description."},"base_url":{"type":"string","maxLength":2000,"format":"uri","example":"https://moodle.example.com"},"lms_kind":{"type":"string","minLength":1,"maxLength":100,"description":"Free-text kind of the server, e.g. \"moodle\" or \"scorm-cloud\"; informational.","example":"moodle"}},"required":["organization_id","slug","name"]},"IngestExternalGradesResult":{"type":"object","properties":{"created":{"type":"integer"},"duplicates":{"type":"integer"},"errors":{"type":"integer"},"outcomes":{"type":"array","items":{"$ref":"#/components/schemas/ExternalGradeOutcome"},"description":"One outcome per submitted row, in row order."}},"required":["created","duplicates","errors","outcomes"]},"ExternalGradeOutcome":{"type":"object","properties":{"index":{"type":"integer","description":"0-based position of the row in the submitted batch (or CSV data rows)."},"status":{"type":"string","enum":["created","duplicate","error"]},"error_description":{"type":["string","null"],"description":"Why the row was rejected; null unless status is error."},"learning_record_id":{"type":["string","null"],"format":"uuid","description":"The completed learning record a created row produced."}},"required":["index","status","error_description","learning_record_id"]},"ExternalGradesImportForm":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"CSV file, at most 1 MB: a header row naming an `external_course_id`, `score` (0-100) and `completed_at` (ISO date) column, plus an `external_user_id` and/or `learner_user_id` column, in any order; `passed` and `external_attempt_id` columns are optional. Header matching is case-insensitive and extra columns are ignored."}},"required":["file"]},"IngestExternalGradesRequest":{"type":"object","properties":{"grades":{"type":"array","items":{"$ref":"#/components/schemas/ExternalGradeRow"},"minItems":1,"maxItems":1000}},"required":["grades"]},"ExternalGradeRow":{"type":"object","properties":{"external_course_id":{"type":"string","minLength":1,"maxLength":200,"description":"The LMS's own course identifier, resolved against the server's catalogue.","example":"SAFETY-101"},"external_user_id":{"type":"string","minLength":1,"maxLength":200,"description":"The LMS's identity of the learner, resolved through the server's user mappings. Provide this or user_id.","example":"jdoe"},"user_id":{"type":"string","format":"uuid","description":"The directory member's users.id, for writers that already know it (e.g. manual entry)."},"score":{"type":"number","minimum":0,"maximum":100,"description":"Normalized 0-100 score of the attempt.","example":87.5},"passed":{"type":"boolean","description":"The LMS's own verdict. Decides passing when the course sets no passing_score."},"completed_at":{"type":"string","format":"date-time","description":"When the attempt was completed on the LMS."},"external_attempt_id":{"type":"string","minLength":1,"maxLength":200,"description":"The LMS's own id of this attempt. The strongest duplicate fence: re-imports of the same id are skipped. Rows without one dedupe on (course, learner, completed_at)."}},"required":["external_course_id","score","completed_at"]},"ExternalCourseGradeList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExternalCourseGrade"}}},"required":["items"]},"ExternalCourseGrade":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"course_id":{"type":"string","format":"uuid"},"course_name":{"type":"string"},"course_external_id":{"type":"string"},"learner_user_id":{"type":"string","format":"uuid"},"external_user_id":{"type":["string","null"]},"external_attempt_id":{"type":["string","null"]},"score":{"type":"number"},"passed":{"type":["boolean","null"]},"completed_at":{"type":"string","format":"date-time"},"learning_record_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["id","course_id","course_name","course_external_id","learner_user_id","external_user_id","external_attempt_id","score","passed","completed_at","learning_record_id","created_at"]},"ExternalLmsUserMappingList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExternalLmsUserMapping"}}},"required":["items"]},"ExternalLmsUserMapping":{"type":"object","properties":{"server_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"external_user_id":{"type":"string"},"user_id":{"type":"string","format":"uuid"},"display_name":{"type":["string","null"],"description":"The member's profile name from users, for listings."},"email":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["server_id","organization_id","external_user_id","user_id","display_name","email","created_at"]},"UpsertExternalLmsUserMappingRequest":{"type":"object","properties":{"external_user_id":{"type":"string","minLength":1,"maxLength":200,"example":"jdoe","description":"The LMS's own identity of the learner. Writing an existing one repoints it."},"user_id":{"type":"string","format":"uuid","description":"users.id of the directory member the identity resolves to."}},"required":["external_user_id","user_id"]},"ExternalLmsCourseList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExternalLmsCourse"}}},"required":["items"]},"ExternalLmsCourse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"server_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"external_course_id":{"type":"string","description":"The LMS's own identifier — the key grades arrive under; unique per server."},"name":{"type":"string"},"description":{"type":["string","null"]},"course_url":{"type":["string","null"]},"passing_score":{"type":["number","null"],"description":"Score required to pass, 0-100. Authoritative when set; when null, the imported passed flag decides."},"subject_matter_id":{"type":["string","null"],"format":"uuid","description":"Subject matter imported grade records are tagged with; null = untagged."},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"archived_at":{"type":["string","null"],"format":"date-time","description":"When set, the record is retired; it stays readable but should not grow."}},"required":["id","server_id","organization_id","external_course_id","name","description","course_url","passing_score","subject_matter_id","created_at","archived_at"]},"CreateExternalLmsCourseRequest":{"type":"object","properties":{"external_course_id":{"type":"string","minLength":1,"maxLength":200,"example":"SAFETY-101","description":"The LMS's own identifier of the course. Unique within the server; immutable."},"name":{"type":"string","minLength":1,"maxLength":200,"example":"Workplace safety basics"},"description":{"type":"string","maxLength":2000},"course_url":{"type":"string","maxLength":2000,"format":"uri","example":"https://moodle.example.com/course/view.php?id=42"},"passing_score":{"type":"number","minimum":0,"maximum":100,"description":"Score required to pass, 0-100. Omit to let the imported passed flag decide.","example":80},"subject_matter_id":{"type":"string","format":"uuid","description":"Subject matter of the type's own organization to tag imported grade records with."}},"required":["external_course_id","name"]},"UpdateExternalLmsServerRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"Send null to clear the description."},"base_url":{"type":["string","null"],"maxLength":2000,"format":"uri","description":"Send null to clear the URL."},"lms_kind":{"type":["string","null"],"minLength":1,"maxLength":100,"description":"Send null to clear the kind.","example":"moodle"}}},"UpdateExternalLmsCourseRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"course_url":{"type":["string","null"],"maxLength":2000,"format":"uri","description":"Send null to clear the URL."},"passing_score":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Send null to clear the pass mark (the imported flag then decides)."},"subject_matter_id":{"type":["string","null"],"format":"uuid","description":"Send null to stop tagging imported records."}}},"AvailableScormCourseList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AvailableScormCourse"}}},"required":["items"]},"AvailableScormCourse":{"type":"object","properties":{"course":{"$ref":"#/components/schemas/ScormCourse"},"organization_name":{"type":"string"},"in_progress_attempt":{"$ref":"#/components/schemas/ScormAttempt"},"latest_attempt":{"allOf":[{"$ref":"#/components/schemas/ScormAttempt"},{"description":"The learner's most recent attempt, in progress or closed."}]},"attempts_used":{"type":"integer"}},"required":["course","organization_name","in_progress_attempt","latest_attempt","attempts_used"]},"ScormCourse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"owner_department_id":{"type":["string","null"],"format":"uuid","description":"Owning department within the organization; null = owned by the organization directly. Scopes which scorm:read/scorm:write grants cover the course, and narrows who may take it: an organization-owned course is open to every member, a department-owned one only to the owning department's role holders."},"name":{"type":"string"},"description":{"type":["string","null"]},"subject_matter_id":{"type":["string","null"],"format":"uuid","description":"Subject matter completions are tagged with; null = untagged."},"scorm_version":{"type":"string","enum":["scorm-1.2","scorm-2004","unknown"],"description":"SCORM revision detected from the package's imsmanifest.xml.","example":"scorm-1.2"},"launch_path":{"type":"string","description":"Zip-entry path (optionally with a query string) of the launch resource."},"mastery_score":{"type":["number","null"],"description":"SCORM 1.2 adlcp:masteryscore (0-100) from the manifest, if declared."},"package_file_name":{"type":"string"},"package_size_bytes":{"type":"integer"},"created_by":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"archived_at":{"type":["string","null"],"format":"date-time","description":"An archived course is hidden from learners and takes no new attempts."}},"required":["id","organization_id","owner_department_id","name","description","subject_matter_id","scorm_version","launch_path","mastery_score","package_file_name","package_size_bytes","created_by","created_at","archived_at"]},"ScormAttempt":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"course_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"attempt_number":{"type":"integer"},"status":{"type":"string","enum":["in_progress","closed"]},"api_flavor":{"type":["string","null"],"enum":["scorm-1.2","scorm-2004",null]},"completion_status":{"type":"string","enum":["completed","incomplete","not_attempted","unknown"]},"success_status":{"type":"string","enum":["passed","failed","unknown"]},"score_raw":{"type":["number","null"]},"score_max":{"type":["number","null"]},"score_scaled":{"type":["number","null"],"description":"0..1 (SCORM 2004 allows -1..1); from cmi.score.scaled, else raw/min/max."},"progress_measure":{"type":["number","null"]},"location":{"type":["string","null"],"description":"Where the learner left off (cmi location), as the module reported it."},"total_time_seconds":{"type":"number","description":"Learning time accumulated across every session of this attempt."},"started_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"last_commit_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"passed":{"type":["boolean","null"],"description":"From success_status: passed/failed => true/false, unknown => null."},"closed_at":{"type":["string","null"],"format":"date-time"},"learning_record_id":{"type":["string","null"],"format":"uuid","description":"The learning record this attempt's completion reported into, if any."}},"required":["id","course_id","organization_id","user_id","attempt_number","status","api_flavor","completion_status","success_status","score_raw","score_max","score_scaled","progress_measure","location","total_time_seconds","started_at","last_commit_at","completed_at","passed","closed_at","learning_record_id"]},"ScormLaunch":{"type":"object","properties":{"attempt":{"$ref":"#/components/schemas/ScormAttempt"},"course":{"$ref":"#/components/schemas/ScormCourse"},"entry":{"type":"string","enum":["ab-initio","resume"],"description":"resume seeds the saved CMI map so the module restores its state."},"cmi":{"type":"object","additionalProperties":{"type":"string","maxLength":65536},"description":"SCORM CMI elements by name, e.g. cmi.core.lesson_status."},"total_time_seconds":{"type":"number"},"objective_ids":{"type":"array","items":{"type":"string"},"description":"Manifest-declared objectives the runtime seeds, like a real LMS."},"mastery_score":{"type":["number","null"]},"learner_id":{"type":"string"},"learner_name":{"type":"string"},"content_url":{"type":"string","description":"Where the launch resource is served from. Relative asset URLs inside the module resolve under the same signed content prefix."}},"required":["attempt","course","entry","cmi","total_time_seconds","objective_ids","mastery_score","learner_id","learner_name","content_url"]},"LaunchScormCourseRequest":{"type":"object","properties":{"restart":{"type":"boolean","description":"True closes any attempt in progress and starts a fresh one; the default resumes it."}}},"CommitScormAttemptRequest":{"type":"object","properties":{"cmi":{"type":"object","additionalProperties":{"type":"string","maxLength":65536},"description":"SCORM CMI elements by name, e.g. cmi.core.lesson_status."},"api_flavor":{"type":["string","null"],"enum":["scorm-1.2","scorm-2004",null]},"terminated":{"type":"boolean","description":"True once the module has called LMSFinish / Terminate this session."}},"required":["cmi","api_flavor","terminated"]},"ScormPackageUploadTicket":{"type":"object","properties":{"pathname":{"type":"string","description":"Server-chosen blob pathname the signed upload URL writes to."},"upload_url":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}},"required":["pathname","upload_url","expires_at"]},"ScormPackageUploadRequest":{"type":"object","properties":{"org":{"type":"string","minLength":1,"description":"Organization UUID id or slug the course will belong to.","example":"acme-corporation"},"department":{"type":"string","minLength":1,"description":"UUID id or slug of the owning department within the organization; omit for a course owned by the organization directly."},"file_name":{"type":"string","minLength":1,"maxLength":255},"size_bytes":{"type":"integer","exclusiveMinimum":0,"maximum":52428800,"description":"Declared package size; the upload token enforces the cap."}},"required":["org","file_name","size_bytes"]},"ScormCourseList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ScormCourse"}}},"required":["items"]},"CreateScormCourseRequest":{"type":"object","properties":{"org":{"type":"string","minLength":1,"description":"Organization UUID id or slug the course belongs to.","example":"acme-corporation"},"department":{"type":"string","minLength":1,"description":"UUID id or slug of the owning department within the organization; omit for a course owned by the organization directly. Ownership scopes administration and limits taking the course to the department's role holders."},"pathname":{"type":"string","pattern":"^scorm-courses\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\/[^/]{1,100}$"},"file_name":{"type":"string","minLength":1,"maxLength":255},"name":{"type":"string","maxLength":200,"description":"Defaults to the title in the package manifest, then the file name."},"description":{"type":"string","maxLength":5000},"subject_matter_id":{"type":"string","format":"uuid"}},"required":["org","pathname","file_name"]},"ScormCourseAttemptList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ScormCourseAttempt"}},"next_cursor":{"type":["string","null"],"description":"Pass as ?cursor= to fetch the next page; null on the last page."}},"required":["items","next_cursor"]},"ScormCourseAttempt":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"course_id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"},"attempt_number":{"type":"integer"},"status":{"type":"string","enum":["in_progress","closed"]},"api_flavor":{"type":["string","null"],"enum":["scorm-1.2","scorm-2004",null]},"completion_status":{"type":"string","enum":["completed","incomplete","not_attempted","unknown"]},"success_status":{"type":"string","enum":["passed","failed","unknown"]},"score_raw":{"type":["number","null"]},"score_max":{"type":["number","null"]},"score_scaled":{"type":["number","null"],"description":"0..1 (SCORM 2004 allows -1..1); from cmi.score.scaled, else raw/min/max."},"progress_measure":{"type":["number","null"]},"location":{"type":["string","null"],"description":"Where the learner left off (cmi location), as the module reported it."},"total_time_seconds":{"type":"number","description":"Learning time accumulated across every session of this attempt."},"started_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"},"last_commit_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"passed":{"type":["boolean","null"],"description":"From success_status: passed/failed => true/false, unknown => null."},"closed_at":{"type":["string","null"],"format":"date-time"},"learning_record_id":{"type":["string","null"],"format":"uuid","description":"The learning record this attempt's completion reported into, if any."},"user_display_name":{"type":["string","null"]},"user_email":{"type":["string","null"]}},"required":["id","course_id","organization_id","user_id","attempt_number","status","api_flavor","completion_status","success_status","score_raw","score_max","score_scaled","progress_measure","location","total_time_seconds","started_at","last_commit_at","completed_at","passed","closed_at","learning_record_id","user_display_name","user_email"]},"UpdateScormCourseRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":5000},"subject_matter_id":{"type":["string","null"],"format":"uuid"},"archived":{"type":"boolean","description":"True archives the course (hidden from learners); false restores it."}}},"CertificationNotificationResult":{"type":"object","properties":{"outcome":{"type":"string","enum":["sent","dry_run","skipped_empty"],"description":"`sent`: the mail server accepted the email. `dry_run`: built and validated, not sent. `skipped_empty`: nothing falls in a window and include_empty was false."},"audience":{"type":"string","enum":["trainee","manager"],"description":"Whose deadlines the digest covers: `trainee` — the user's own certification requirements; `manager` — the requirements of every trainee holding a role the user manages (scoped to those managed roles).","example":"trainee"},"user_id":{"type":"string","format":"uuid","description":"Who the digest covers."},"to":{"type":["string","null"],"description":"The recipient address used, or null when the digest was skipped."},"subject":{"type":["string","null"],"description":"The email's subject line, or null when the digest was skipped."},"total_items":{"type":"integer","minimum":0,"description":"Requirements in the digest across all windows."},"window_counts":{"type":"object","properties":{"overdue":{"type":"integer","minimum":0},"next_10_days":{"type":"integer","minimum":0},"next_30_days":{"type":"integer","minimum":0},"next_60_days":{"type":"integer","minimum":0}},"required":["overdue","next_10_days","next_30_days","next_60_days"],"description":"Requirements per window: overdue, and due in 10/30/60 days."},"preview_text":{"type":["string","null"],"description":"The plaintext body that was (or, for dry_run, would have been) sent; the HTML variant is rendered from the same digest."}},"required":["outcome","audience","user_id","to","subject","total_items","window_counts","preview_text"]},"SendCertificationNotificationRequest":{"type":"object","properties":{"user":{"type":"string","format":"uuid","description":"`users.id` of the user the digest is about — and, unless `email` overrides it, the recipient (resolved from their account's profile).","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"audience":{"type":"string","enum":["trainee","manager"],"description":"Whose deadlines the digest covers: `trainee` — the user's own certification requirements; `manager` — the requirements of every trainee holding a role the user manages (scoped to those managed roles).","example":"trainee"},"email":{"type":"string","format":"email","description":"Send to this address instead of the user's account email.","example":"someone@example.com"},"dry_run":{"type":"boolean","description":"Build, render and validate the email without contacting the mail server. Defaults to false."},"include_empty":{"type":"boolean","description":"Send an \"all clear\" email even when no requirement is overdue or due within 60 days. Defaults to false (such digests are skipped)."}},"required":["user","audience"]},"DeploymentBranding":{"type":"object","properties":{"display_name":{"type":["string","null"],"description":"The long-form deployment name shown in the footer, page titles and emails, or null when the short name (or, failing that, the Botree default) is shown.","example":"Acme Corporation Learning Record Storage"},"short_name":{"type":["string","null"],"description":"The short name shown as the dashboard wordmark and top bar title, or null when the long-form name (or, failing that, the Botree default) is shown.","example":"Acme Corp LRS"},"logo":{"$ref":"#/components/schemas/DeploymentBrandingLogo"},"updated_at":{"type":"string","format":"date-time","example":"2026-01-01T00:00:00.000Z"}},"required":["display_name","short_name","logo","updated_at"]},"DeploymentBrandingLogo":{"type":["object","null"],"properties":{"file_name":{"type":"string","description":"The uploaded file's original name."},"content_type":{"type":"string","description":"The image's content type."},"url":{"type":"string","description":"Where the logo is served from, with a cache-busting version query. Public: the header and footer load it as a plain image.","example":"/api/branding/logo?v=1735689600000"}},"required":["file_name","content_type","url"],"description":"The uploaded logo, or null when the Botree artwork is shown."},"UpdateDeploymentBrandingRequest":{"type":"object","properties":{"display_name":{"type":["string","null"],"maxLength":100,"description":"The long-form name to show; send null or an empty string to restore the default. Omit to leave it unchanged.","example":"Acme Corporation Learning Record Storage"},"short_name":{"type":["string","null"],"maxLength":40,"description":"The short name for the dashboard wordmark and top bar; send null or an empty string to fall back to the long-form name. Omit to leave it unchanged.","example":"Acme Corp LRS"}}},"DeploymentBrandingLogoForm":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"The logo image, as a file part: PNG, JPEG, WebP, GIF or SVG, at most 2 MB. Square artwork fits the header best."}},"required":["file"]}},"parameters":{}},"paths":{"/api/auth/token-info":{"get":{"tags":["auth"],"summary":"Describe the presented access token","description":"Example protected endpoint: echoes back the claims of the bearer token after it has been validated against the OIDC provider.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The validated access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenInfo"}}}},"400":{"description":"The Authorization header does not use the Bearer scheme.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token is missing a required scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The access token could not be validated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations":{"post":{"tags":["directory"],"summary":"Create an organization","description":"Creates a new top-level organization. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrganizationRequest"}}}},"responses":{"201":{"description":"The created organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"An organization with that slug already exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["directory"],"summary":"List organizations","description":"Lists every organization in the directory, archived ones included. Requires a valid access token.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Every organization, ordered by name.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationSummary"}}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}":{"patch":{"tags":["directory"],"summary":"Update an organization","description":"Changes an organization's slug, name or description; omitted fields keep their values. Requires a superuser's access token, like creating one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrganizationRequest"}}}},"responses":{"200":{"description":"The updated organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No organization has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Another organization already has that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments":{"post":{"tags":["directory"],"summary":"Create a department","description":"Creates a department in an organization, optionally nested under a parent department in the same organization. Requires a superuser's access token, or one whose subject holds the directory:write permission in a scope covering the parent — the organization for a top-level department, the parent department for a nested one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDepartmentRequest"}}}},"responses":{"201":{"description":"The created department.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is neither a superuser nor a directory:write holder whose scope covers the parent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, or the parent department is not in that organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a department with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments/{departmentId}":{"patch":{"tags":["directory"],"summary":"Update a department","description":"Changes a department's slug, name or description, or moves it under another parent in the same organization; omitted fields keep their values. Requires a superuser's access token, or one whose subject holds the directory:write permission in a scope covering the department — and, when moving it, the new parent too.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDepartmentRequest"}}}},"responses":{"200":{"description":"The updated department.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is neither a superuser nor a directory:write holder whose scope covers the department (and the new parent, when moving it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, the department is not in that organization, or the new parent is not in that organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a department with that slug, or the move would nest the department beneath itself.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments/{departmentId}/roles":{"post":{"tags":["directory"],"summary":"Create a department role","description":"Creates a role in a department of an organization, with the permission strings it grants. Requires a superuser's access token, or one whose subject holds the directory:write permission in a scope covering the department.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDepartmentRoleRequest"}}}},"responses":{"201":{"description":"The created role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentRole"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is neither a superuser nor a directory:write holder whose scope covers the department.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, or the department is not in that organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The department already has a role with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments/{departmentId}/roles/{roleId}":{"patch":{"tags":["directory"],"summary":"Update a department role","description":"Changes a role's slug, name or description, or replaces the permission set it grants; omitted fields keep their values. Requires a superuser's access token, or one whose subject holds the directory:write permission in a scope covering the department.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"4c1d2f6a-8e0b-45c7-9a92-6d84a4f4c9b1"},"required":true,"name":"roleId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDepartmentRoleRequest"}}}},"responses":{"200":{"description":"The updated role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentRole"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is neither a superuser nor a directory:write holder whose scope covers the department.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, the department is not in that organization, or the department has no role with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The department already has a role with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/users/import":{"post":{"tags":["directory"],"summary":"Import directory members from a CSV file","description":"Adds every row of an uploaded CSV file to the organization's user directory in one request. The file needs a header row naming a user id, name and email column; rows whose subject is already in the directory are skipped rather than overwritten, so an import can safely be re-run. A file with any invalid row imports nothing and reports the problems, so a corrected file can simply be uploaded again. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/DirectoryImportForm"}}}},"responses":{"200":{"description":"The import's outcome: what was added and what was already present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryImportSummary"}}}},"400":{"description":"The file is not parseable CSV, a required column is missing from the header, a row failed validation, a subject appears twice, or the file has no (or too many) data rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No organization or sign-in provider has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A concurrent sign-in or import raced this one; re-run the import.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"The file is larger than 1 MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/users":{"get":{"tags":["directory"],"summary":"List an organization's user directory","description":"Lists every member of the organization's user directory, named members first, each with the number of roles they currently hold. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"}],"responses":{"200":{"description":"The directory's members.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DirectoryUser"}}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No organization has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["directory"],"summary":"Add a user to the directory","description":"Adds a user to the organization's directory: an existing account by user_id, or a pre-provisioned identity by provider_id + subject — the account is created on the spot when that identity is new. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddDirectoryUserRequest"}}}},"responses":{"201":{"description":"The added member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUser"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, the named user, or the named provider does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That user is already in the organization's directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/users/{user_id}":{"get":{"tags":["directory"],"summary":"Get a directory member","description":"Returns one member of the organization's directory, with every role grant they hold there — expired ones included. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The member's account id (`users.id`).","name":"user_id","in":"path"}],"responses":{"200":{"description":"The member and their grants.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUserWithRoles"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, or that user is not in its directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["directory"],"summary":"Remove a user from the directory","description":"Removes a member from the organization's directory, revoking every role they hold there. The account itself (and its history) remains. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The member's account id (`users.id`).","name":"user_id","in":"path"}],"responses":{"200":{"description":"The removed member, with the grants that were revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUserWithRoles"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, or that user is not in its directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/users/{user_id}/roles":{"get":{"tags":["directory"],"summary":"List a directory member's roles","description":"Lists every role grant a member holds in the organization, expired ones included — check expires_at. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The member's account id (`users.id`).","name":"user_id","in":"path"}],"responses":{"200":{"description":"The member's grants, by department and role name.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DirectoryUserRole"}}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization does not exist, or that user is not in its directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["directory"],"summary":"Assign a role to a directory member","description":"Grants a member one of the roles defined by a department of the organization, open-ended or until an expiry. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The member's account id (`users.id`).","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignDirectoryUserRoleRequest"}}}},"responses":{"201":{"description":"The created grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUserRole"}}}},"400":{"description":"The request body failed validation, or expires_at is not in the future.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, the member in its directory, the department, or the role does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The member already holds that role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/users/{user_id}/roles/{role_id}":{"patch":{"tags":["directory"],"summary":"Update a directory member's role grant","description":"Changes when a member's grant expires — renewing a lapsed one, scheduling an end, or making it open-ended with null. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The member's account id (`users.id`).","name":"user_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Role UUID id or slug, resolved among the member's grants. A member holding identically-slugged roles in two departments must be addressed by the UUID.","example":"reviewer"},"required":true,"description":"Role UUID id or slug, resolved among the member's grants. A member holding identically-slugged roles in two departments must be addressed by the UUID.","name":"role_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDirectoryUserRoleRequest"}}}},"responses":{"200":{"description":"The updated grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUserRole"}}}},"400":{"description":"The request body failed validation, expires_at does not follow the grant time, or the role slug is ambiguous across the member's departments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, the member in its directory, or the member's grant of that role does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["directory"],"summary":"Revoke a directory member's role","description":"Removes one of a member's role grants. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The member's account id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The member's account id (`users.id`).","name":"user_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Role UUID id or slug, resolved among the member's grants. A member holding identically-slugged roles in two departments must be addressed by the UUID.","example":"reviewer"},"required":true,"description":"Role UUID id or slug, resolved among the member's grants. A member holding identically-slugged roles in two departments must be addressed by the UUID.","name":"role_id","in":"path"}],"responses":{"200":{"description":"The revoked grant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DirectoryUserRole"}}}},"400":{"description":"The role slug is ambiguous across the member's departments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, the member in its directory, or the member's grant of that role does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments/{departmentId}/roles/{roleId}/managers":{"get":{"tags":["directory"],"summary":"List a role's reporting lines","description":"Lists the roles managing this role and the roles it manages. Holders of a manager role (or of any role above it in the chain) may approve or reject the pending learning-record submissions of the subordinate role's holders.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"9c2f1f7e-4a35-4b8f-8d21-3f5b2a7c9d10"},"required":true,"name":"roleId","in":"path"}],"responses":{"200":{"description":"The role's reporting lines, both directions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleManagers"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department, or role does not exist there.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["directory"],"summary":"Make a role a manager of this role","description":"Adds a reporting line: the named role becomes a manager of the role in the path. Both roles must belong to the same organization; a line that would close a cycle is refused. Requires a superuser's access token, or one whose user holds directory:write in a scope covering both roles' departments.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"9c2f1f7e-4a35-4b8f-8d21-3f5b2a7c9d10"},"required":true,"name":"roleId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleManagerRequest"}}}},"responses":{"201":{"description":"The created reporting line, manager side resolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleManagerEdge"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller is neither a superuser nor a directory:write holder whose scope covers both roles' departments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department, or role does not exist there, or the manager role is not in that organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The role already reports to that manager, or the line would close a cycle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments/{departmentId}/roles/{roleId}/managers/{managerRoleId}":{"delete":{"tags":["directory"],"summary":"Remove a manager from this role","description":"Removes the reporting line between this role and the named manager role. Requires a superuser's access token, or one whose user holds directory:write in a scope covering both roles' departments.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"9c2f1f7e-4a35-4b8f-8d21-3f5b2a7c9d10"},"required":true,"name":"roleId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"2e6d0c4a-8b1f-4e7a-9c3d-5a4b6c7d8e9f"},"required":true,"name":"managerRoleId","in":"path"}],"responses":{"204":{"description":"The reporting line is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller is neither a superuser nor a directory:write holder whose scope covers both roles' departments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such reporting line — a path segment or the line itself does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}":{"get":{"tags":["directory"],"summary":"Get an organization","description":"Returns one organization and every department inside it. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"}],"responses":{"200":{"description":"The organization and its departments.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationWithDepartments"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No organization has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/departments/{dept_id}":{"get":{"tags":["directory"],"summary":"Get a department","description":"Returns one department of an organization. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within the organization.","example":"quality-assurance"},"required":true,"description":"Department UUID id or slug, resolved within the organization.","name":"dept_id","in":"path"}],"responses":{"200":{"description":"The department.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentSummary"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, or the department within it, does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/departments/{dept_id}/roles":{"get":{"tags":["directory"],"summary":"List a department's roles","description":"Lists the roles a department defines, with the permissions each grants and how many users currently hold it. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within the organization.","example":"quality-assurance"},"required":true,"description":"Department UUID id or slug, resolved within the organization.","name":"dept_id","in":"path"}],"responses":{"200":{"description":"The department's roles, ordered by name.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DepartmentRoleSummary"}}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, or the department within it, does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/departments/{dept_id}/roles/{role_id}":{"get":{"tags":["directory"],"summary":"Get a department role","description":"Returns one role of a department, with the permissions it grants and how many users currently hold it. Requires a valid access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within the organization.","example":"quality-assurance"},"required":true,"description":"Department UUID id or slug, resolved within the organization.","name":"dept_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Role UUID id or slug, resolved within the department.","example":"reviewer"},"required":true,"description":"Role UUID id or slug, resolved within the department.","name":"role_id","in":"path"}],"responses":{"200":{"description":"The role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentRoleSummary"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department or role does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The directory database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/providers":{"get":{"tags":["providers"],"summary":"List the sign-in providers","description":"Every registered OpenID Connect provider, disabled ones included. Rows name the env vars their secrets live in; the secrets themselves are never served.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The registered providers, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInProviderList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The provider catalogue could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["providers"],"summary":"Register a sign-in provider","description":"Registers an OpenID Connect provider whose sign-ins and access tokens this deployment accepts. Secrets stay in the environment: the row names the variables to read. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSignInProviderRequest"}}}},"responses":{"201":{"description":"The registered provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInProvider"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The slug or issuer is taken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The provider catalogue could not be written to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/providers/{provider_id}":{"get":{"tags":["providers"],"summary":"Read one sign-in provider","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Provider UUID id or slug.","example":"acme-sso"},"required":true,"description":"Provider UUID id or slug.","name":"provider_id","in":"path"}],"responses":{"200":{"description":"The provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInProvider"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No provider has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The provider catalogue could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["providers"],"summary":"Update a sign-in provider","description":"Changes a provider's settings; omitted fields keep their values. Disabling a provider stops its sign-ins and token verification while keeping its identities. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Provider UUID id or slug.","example":"acme-sso"},"required":true,"description":"Provider UUID id or slug.","name":"provider_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSignInProviderRequest"}}}},"responses":{"200":{"description":"The updated provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInProvider"}}}},"400":{"description":"The request body failed validation, or the settings violate a constraint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No provider has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The slug or issuer is taken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The provider catalogue could not be written to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["providers"],"summary":"Delete a sign-in provider","description":"Removes a provider nothing references. A provider whose identities still exist cannot be deleted — disable it, or detach the identities first. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Provider UUID id or slug.","example":"acme-sso"},"required":true,"description":"Provider UUID id or slug.","name":"provider_id","in":"path"}],"responses":{"200":{"description":"The deleted provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInProvider"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No provider has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Sign-in identities still reference the provider.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The provider catalogue could not be written to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/me":{"get":{"tags":["users"],"summary":"Read the caller's account","description":"The account the presented token resolves to: canonical profile and sign-in methods. Accounts come into existence on first contact, so this always answers for a valid token.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The caller's account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserProfile"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The account could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/me/identities/{identity_id}":{"delete":{"tags":["users"],"summary":"Unlink one of the caller's sign-in methods","description":"Detaches a sign-in method from the caller's own account. The last method cannot be unlinked — that would lock the account out; ask a superuser to detach it deliberately.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The identity's id, from the account's identity listing."},"required":true,"description":"The identity's id, from the account's identity listing.","name":"identity_id","in":"path"}],"responses":{"200":{"description":"The unlinked sign-in method.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserIdentity"}}}},"400":{"description":"The identity is the account's last sign-in method.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The identity does not exist, or belongs to another account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The identity could not be detached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/{user_id}":{"patch":{"tags":["users"],"summary":"Update an account's profile","description":"Sets the account's display name (null clears it); omitted fields keep their values. The name is the canonical profile the app shows everywhere, and a later sign-in whose ID token carries a name claim refreshes it again. A merged (tombstoned) account cannot be edited — its survivor holds the profile. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The account's id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The account's id (`users.id`).","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}}},"responses":{"200":{"description":"The updated account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserAccount"}}}},"400":{"description":"The request body failed validation, or the account was merged away.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No user has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The account could not be updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/{user_id}/identities":{"get":{"tags":["users"],"summary":"List an account's sign-in methods","description":"Every identity attached to the account. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The account's id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The account's id (`users.id`).","name":"user_id","in":"path"}],"responses":{"200":{"description":"The account's sign-in methods, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserIdentity"}}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No user has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The identities could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["users"],"summary":"Attach a sign-in method to an account","description":"Links a (provider, subject) pair to the account — the administrative side of identity linking, for provider migrations and rescues. An identity already attached to a *different* account is refused: merge or detach it first. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The account's id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The account's id (`users.id`).","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachIdentityRequest"}}}},"responses":{"201":{"description":"The attached sign-in method.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserIdentity"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No user or provider has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That identity already belongs to a different account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The identity could not be attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/{user_id}/identities/{identity_id}":{"delete":{"tags":["users"],"summary":"Detach a sign-in method from an account","description":"Removes one identity from the account. Detaching the *last* one locks the account out of signing in (its history remains) and is refused unless `force=true` — the deliberate offboarding or compromised-credential case. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The account's id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The account's id (`users.id`).","name":"user_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"The identity's id, from the account's identity listing."},"required":true,"description":"The identity's id, from the account's identity listing.","name":"identity_id","in":"path"},{"schema":{"type":"string","enum":["true","false"],"description":"Set true to detach the account's last sign-in method."},"required":false,"description":"Set true to detach the account's last sign-in method.","name":"force","in":"query"}],"responses":{"200":{"description":"The detached sign-in method.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserIdentity"}}}},"400":{"description":"The identity is the account's last sign-in method and force is not set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The user or the identity does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The identity could not be detached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/users/{user_id}/merge":{"post":{"tags":["users"],"summary":"Merge an account into another","description":"Absorbs the account at {user_id} into into_user_id: identities move over, every reference is repointed at the survivor (redundant memberships, grants and duplicates resolve in the survivor's favour), and the absorbed account is tombstoned. The fix for the duplicate a first sign-in creates before its identity is linked. One transaction, irreversible. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The account's id (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":true,"description":"The account's id (`users.id`).","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeUsersRequest"}}}},"responses":{"200":{"description":"The merge's outcome.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeUsersResponse"}}}},"400":{"description":"The merge is refused: a self-merge, or a tombstoned participant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"One of the accounts does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The merge failed; nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/subjects":{"get":{"tags":["subjects"],"summary":"List subject matters","description":"Lists the subject matters the caller can read, newest first, with cursor pagination. Requires subjects:read in scope; rows outside the caller's scope are simply absent.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of subject matters, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubjectMatterList"}}}},"400":{"description":"A filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["subjects"],"summary":"Create a subject matter","description":"Creates a subject matter owned by an organization, or by one department within it. Requires subjects:write over the owner.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubjectMatterRequest"}}}},"responses":{"201":{"description":"The created subject matter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubjectMatter"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold subjects:write over the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, or the department within it, does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a subject with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/subjects/{subject_id}":{"get":{"tags":["subjects"],"summary":"Get a subject matter","description":"Returns one subject matter, by UUID id or by slug resolved within ?org=. Requires subjects:read over its owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"subject_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The subject matter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubjectMatter"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible subject matter has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["subjects"],"summary":"Update a subject matter","description":"Changes a subject matter's slug, name or description. Requires subjects:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"subject_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSubjectMatterRequest"}}}},"responses":{"200":{"description":"The updated subject matter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubjectMatter"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this subject but does not hold subjects:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible subject matter has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a subject with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["subjects"],"summary":"Archive a subject matter","description":"Soft-deletes a subject matter by setting archived_at; records filed under it keep their tags. Requires subjects:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"subject_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The archived subject matter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubjectMatter"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this subject but does not hold subjects:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible subject matter has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records/submissions":{"post":{"tags":["records"],"summary":"Submit a record and its evidence in one request","description":"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.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/LearningRecordSubmissionForm"}}}},"responses":{"201":{"description":"The created record and its evidence rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecordSubmission"}}}},"400":{"description":"A part failed validation, or the file part does not match the declared file evidence.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold records:write over the owner, and this is not a pending submission about themselves.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department, learner, or a tagged subject does not exist there.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a record with that external_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The file could not be stored, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records":{"get":{"tags":["records"],"summary":"List learning records","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","format":"uuid","description":"Only records about this learner (`users.id`).","example":"8f14e45f-ceea-467f-a10e-cbb9f6bcaf0f"},"required":false,"description":"Only records about this learner (`users.id`).","name":"learner","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Only records filed under this subject matter: UUID id, or slug resolved within org."},"required":false,"description":"Only records filed under this subject matter: UUID id, or slug resolved within org.","name":"subject","in":"query"},{"schema":{"type":"string","enum":["pending","rejected","in_progress","completed","expired","revoked"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","enum":["score","date_range","quantity","url","file","pass_fail","note"],"description":"Only records carrying at least one evidence row of this type."},"required":false,"description":"Only records carrying at least one evidence row of this type.","name":"evidence_type","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of records, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecordList"}}}},"400":{"description":"A filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["records"],"summary":"Create a learning record","description":"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.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLearningRecordRequest"}}}},"responses":{"201":{"description":"The created record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecord"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold records:write over the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department, learner, or a tagged subject does not exist there.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a record with that external_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records/{record_id}":{"get":{"tags":["records"],"summary":"Get a learning record","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"responses":{"200":{"description":"The record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecord"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["records"],"summary":"Archive a learning record","description":"Soft-deletes a record by setting archived_at; its evidence and blobs are kept, so it can be un-archived. Requires records:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"responses":{"200":{"description":"The archived record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecord"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this record but does not hold records:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["records"],"summary":"Update a learning record","description":"Changes a record's fields, and replaces its subject set when subject_matter_ids is present. Requires records:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLearningRecordRequest"}}}},"responses":{"200":{"description":"The updated record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecord"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this record but does not hold records:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a record with that external_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records/{record_id}/approve":{"post":{"tags":["records"],"summary":"Approve a pending learning record","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApproveLearningRecordRequest"}}}},"responses":{"200":{"description":"The approved record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecord"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller neither holds records:write over the record's owner nor is in the learner's manager chain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The record is not pending — already approved, or archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records/{record_id}/reject":{"post":{"tags":["records"],"summary":"Reject a pending learning record","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectLearningRecordRequest"}}}},"responses":{"200":{"description":"The rejected record, carrying the note for its learner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LearningRecord"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller neither holds records:write over the record's owner nor is in the learner's manager chain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The record is not pending — already decided, or archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records/{record_id}/evidence":{"get":{"tags":["records"],"summary":"List a record's evidence","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"responses":{"200":{"description":"The record's evidence, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupportingEvidence"}}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["records"],"summary":"Add evidence to a record","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupportingEvidenceRequest"}}}},"responses":{"201":{"description":"The created evidence row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportingEvidence"}}}},"400":{"description":"The payload failed validation, or the uploaded blob is missing or does not match it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this record but does not hold records:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That upload has already been attached to an evidence row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/records/{record_id}/evidence/uploads":{"post":{"tags":["records"],"summary":"Request an upload URL for file evidence","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"record_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadTicketRequest"}}}},"responses":{"201":{"description":"Where to PUT the file, and the evidence id minted for it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadTicket"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this record but does not hold records:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible record has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The upload URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/evidence/{evidence_id}":{"patch":{"tags":["records"],"summary":"Update an evidence row","description":"Changes an evidence row's label or replaces its payload (same evidence_type; not for files). Requires records:write over the owning record.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"evidence_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupportingEvidenceRequest"}}}},"responses":{"200":{"description":"The updated evidence row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportingEvidence"}}}},"400":{"description":"The payload failed validation, changed type, or replaced a file payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read the record but does not hold records:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible evidence row has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["records"],"summary":"Delete an evidence row","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"evidence_id","in":"path"}],"responses":{"204":{"description":"The evidence row (and its blob, if any) is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read the record but does not hold records:write over it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible evidence row has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/evidence/{evidence_id}/download":{"get":{"tags":["records"],"summary":"Get a download URL for file evidence","description":"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.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"evidence_id","in":"path"}],"responses":{"200":{"description":"Where to fetch the file from, for the next five minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadTicket"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible evidence row has that id, or it has no file attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The download URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/status":{"get":{"tags":["certification-status"],"summary":"Evaluate certification status","description":"Evaluates every certification requirement of a user: the types their unexpired role grants require, the current award, the due date, and per-rule progress since the current award. Any bearer user may read their own status; reading another user's requires certifications:read, and returns only types in the caller's scope.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"Evaluate this user (`users.id`) instead of the caller. Requires certifications:read; the result is filtered to types in the caller's scope."},"required":false,"description":"Evaluate this user (`users.id`) instead of the caller. Requires certifications:read; the result is filtered to types in the caller's scope.","name":"user","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The user's evaluated certification requirements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCertificationStatusList"}}}},"400":{"description":"The org filter did not resolve.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Another user was asked for without certifications:read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/claim":{"post":{"tags":["certification-status"],"summary":"Claim an automatic certification","description":"Awards the caller a certification of an automatic-mode type they are required to hold, after the server re-validates that every proof rule is satisfied. Answers 409 when a rule is not met, or when the current award changed under the claim (retry after re-reading status).","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"}],"responses":{"201":{"description":"The new award.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationAward"}}}},"400":{"description":"The type is awarded by approval — file a request instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"No unexpired role grant of the caller requires this type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"That certification type does not exist, or is archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A proof rule is not satisfied, or the current award changed underneath.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/request":{"post":{"tags":["certification-status"],"summary":"Request a certification","description":"Files a request for an approval-mode type the caller is required to hold, to be decided by a holder of certifications:write. One pending request per user and type.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCertificationRequest"}}}},"responses":{"201":{"description":"The filed request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRequest"}}}},"400":{"description":"The type is automatic — claim it instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"No unexpired role grant of the caller requires this type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"That certification type does not exist, or is archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A request is already pending, or the caller is not in the organization's directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-requests/{request_id}/withdraw":{"post":{"tags":["certification-status"],"summary":"Withdraw an own request","description":"Withdraws the caller's own still-pending request.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification request."},"required":true,"description":"UUID id of the certification request.","name":"request_id","in":"path"}],"responses":{"200":{"description":"The withdrawn request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRequest"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No request of the caller has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The request has already been decided.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-requests/{request_id}/approve":{"post":{"tags":["certification-status"],"summary":"Approve a request","description":"Approves a pending request: inserts the award and stamps the request in one transaction. The deciding human is the authority — rules are not re-checked. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification request."},"required":true,"description":"UUID id of the certification request.","name":"request_id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideCertificationRequestRequest"}}}},"responses":{"200":{"description":"The approved request, with the resulting award's id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRequest"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this request but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible request has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The request has already been decided or withdrawn.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-requests/{request_id}/reject":{"post":{"tags":["certification-status"],"summary":"Reject a request","description":"Rejects a pending request. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification request."},"required":true,"description":"UUID id of the certification request.","name":"request_id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideCertificationRequestRequest"}}}},"responses":{"200":{"description":"The rejected request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRequest"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this request but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible request has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The request has already been decided or withdrawn.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-requests":{"get":{"tags":["certification-status"],"summary":"List certification requests","description":"The approvals queue: requests whose certification type the caller holds certifications:write over, newest first, with cursor pagination. Defaults to pending requests only.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["pending","approved","rejected","withdrawn"],"description":"Filter requests by status. Defaults to pending — the open queue."},"required":false,"description":"Filter requests by status. Defaults to pending — the open queue.","name":"status","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of requests, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRequestList"}}}},"400":{"description":"The org filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/grants":{"post":{"tags":["certification-status"],"summary":"Grant a certification directly","description":"Awards a certification to a user without a request — for paper history, external audits, or admin discretion. `awarded_at` may be backdated; the recert clock counts from it. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantCertificationRequest"}}}},"responses":{"201":{"description":"The new award.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationAward"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this type but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/awards":{"get":{"tags":["certification-status"],"summary":"List a type's awards","description":"Lists the award events of one certification type, newest first, revoked ones included. Requires certifications:read over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"}],"responses":{"200":{"description":"The most recent awards of the type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationAwardList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-awards/{award_id}/revoke":{"post":{"tags":["certification-status"],"summary":"Revoke an award","description":"Revokes one award. The row stays as history; the user's status reverts to outstanding (or to their previous unrevoked award). Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the award."},"required":true,"description":"UUID id of the award.","name":"award_id","in":"path"}],"responses":{"200":{"description":"The revoked award.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationAward"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this award but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible award has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The award is already revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/due-dates":{"get":{"tags":["certification-status"],"summary":"List a type's initial due dates","description":"Lists the admin-set per-person initial due dates of one certification type, soonest first. Requires certifications:read over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"}],"responses":{"200":{"description":"The type's due dates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationDueDateList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/due-dates/{user_id}":{"put":{"tags":["certification-status"],"summary":"Set a member's initial due date","description":"Sets (or replaces) when one organization member's initial certification falls due, overriding any role-level initial due date. Requires certifications:write over the type's owner. The member must be in the organization's directory.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"`users.id` of the organization member."},"required":true,"description":"`users.id` of the organization member.","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCertificationDueDateRequest"}}}},"responses":{"200":{"description":"The stored due date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationDueDate"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this type but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id, or the user is not in the organization's directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["certification-status"],"summary":"Clear a member's initial due date","description":"Clears a member's per-person initial due date; their requirement falls back to the role-level initial due date, or shows \"No due date set\" when no role sets one. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the certification type."},"required":true,"description":"UUID id of the certification type.","name":"cert_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"`users.id` of the organization member."},"required":true,"description":"`users.id` of the organization member.","name":"user_id","in":"path"}],"responses":{"200":{"description":"The removed due date.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationDueDate"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this type but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id, or no due date was set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-rules/{rule_id}/sign-offs":{"post":{"tags":["certification-status"],"summary":"Record a sign-off","description":"Records that a user satisfies one manual_sign_off rule. `signed_off_at` may be backdated; a renewal needs a sign-off after the current award. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the proof rule."},"required":true,"description":"UUID id of the proof rule.","name":"rule_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCertificationSignOffRequest"}}}},"responses":{"201":{"description":"The recorded sign-off.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationSignOff"}}}},"400":{"description":"The rule is not a manual_sign_off rule, or the body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this rule but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible rule has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["certification-status"],"summary":"List a rule's sign-offs","description":"Lists the sign-offs recorded against one rule, newest first, optionally for one user. Requires certifications:read over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the proof rule."},"required":true,"description":"UUID id of the proof rule.","name":"rule_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"Evaluate this user (`users.id`) instead of the caller. Requires certifications:read; the result is filtered to types in the caller's scope."},"required":false,"description":"Evaluate this user (`users.id`) instead of the caller. Requires certifications:read; the result is filtered to types in the caller's scope.","name":"user","in":"query"}],"responses":{"200":{"description":"The rule's sign-offs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationSignOffList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible rule has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-sign-offs/{sign_off_id}":{"delete":{"tags":["certification-status"],"summary":"Delete a sign-off","description":"Hard-deletes a mistaken sign-off, the way evidence is deleted. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the sign-off."},"required":true,"description":"UUID id of the sign-off.","name":"sign_off_id","in":"path"}],"responses":{"200":{"description":"The deleted sign-off.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationSignOff"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this sign-off but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible sign-off has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications":{"get":{"tags":["certifications"],"summary":"List certification types","description":"Lists the certification types the caller can read, newest first, with cursor pagination. Requires certifications:read in scope; rows outside the caller's scope are simply absent.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of certification types, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationTypeList"}}}},"400":{"description":"A filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["certifications"],"summary":"Create a certification type","description":"Creates a certification type owned by an organization, or by one department within it, tied to a subject matter of the same organization. Requires certifications:write over the owner.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCertificationTypeRequest"}}}},"responses":{"201":{"description":"The created certification type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationType"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold certifications:write over the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, the department within it, or the subject matter does not exist (or the subject belongs to another organization).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a certification type with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/attachment-uploads":{"post":{"tags":["certifications"],"summary":"Request an upload URL for a certification type attachment","description":"Issues a presigned URL to PUT one file of any type straight to private blob storage, scoped to this certification type, the declared content type and a 500 MB ceiling. Attach it with POST /certifications/{cert_id}/attachments and the returned pathname afterwards. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDescriptionUploadRequest"}}}},"responses":{"201":{"description":"Where to PUT the file, and the pathname the attachment will point at.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionUploadTicket"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this certification type but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type matches the path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The upload URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/attachments":{"post":{"tags":["certifications"],"summary":"Attach an uploaded file to a certification type","description":"Creates the attachment at the end of the authored order, pointing at a pathname an upload ticket was issued for (after PUTting the file there). The content type is read back from the store, never from the request. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDescriptionAttachmentRequest"}}}},"responses":{"201":{"description":"The created attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionAttachment"}}}},"400":{"description":"The body failed validation, or the pathname was not uploaded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this certification type but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type matches the path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That upload is already attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/attachments/{attachment_id}/download":{"get":{"tags":["certifications"],"summary":"Get a download URL for a certification type attachment","description":"Issues a short-lived presigned GET for the private attachment. Readable by anyone an unexpired role grant requires the type of, and by holders of certifications:read over its owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"Where to fetch the file from, for the next five minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadTicket"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The download URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/attachments/{attachment_id}":{"patch":{"tags":["certifications"],"summary":"Relabel or reorder a certification type attachment","description":"Changes the attachment's label (null or empty clears it back to the file name) or its slot in the authored order. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDescriptionAttachmentRequest"}}}},"responses":{"200":{"description":"The updated attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionAttachment"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this certification type but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["certifications"],"summary":"Delete a certification type attachment","description":"Removes the attachment; the blob it pointed at is deleted best-effort. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"204":{"description":"The attachment is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this certification type but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}":{"get":{"tags":["certifications"],"summary":"Get a certification type","description":"Returns one certification type with its proof rules, by UUID id or by slug resolved within ?org=. Requires certifications:read over its owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The certification type and its rules.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationTypeDetail"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["certifications"],"summary":"Update a certification type","description":"Changes a certification type's slug, name, description or awarding mode. The subject matter is immutable. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCertificationTypeRequest"}}}},"responses":{"200":{"description":"The updated certification type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationType"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this type but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a certification type with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["certifications"],"summary":"Archive a certification type","description":"Soft-deletes a certification type by setting archived_at. Archived types stop being evaluated as requirements; awards already given stay readable. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The archived certification type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationType"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this type but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certifications/{cert_id}/rules":{"post":{"tags":["certifications"],"summary":"Add a proof rule","description":"Adds one proof rule to a certification type; all rules of a type must be satisfied at once for it to be awardable. Note that date_range evidence counts its full elapsed wall-clock time towards subject_hours rules, so size thresholds accordingly. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"forklift-operator"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"cert_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCertificationRuleRequest"}}}},"responses":{"201":{"description":"The created rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRule"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this type but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible certification type has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/certification-rules/{rule_id}":{"patch":{"tags":["certifications"],"summary":"Update a proof rule","description":"Changes a rule's label or configuration. The rule kind is immutable — delete and recreate a rule that should mean something else. Requires certifications:write over the type's owner. Progress is recomputed retroactively; award history is unaffected.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the proof rule."},"required":true,"description":"UUID id of the proof rule.","name":"rule_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCertificationRuleRequest"}}}},"responses":{"200":{"description":"The updated rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRule"}}}},"400":{"description":"The request body failed validation, or does not fit the rule's kind.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this rule but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible rule has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["certifications"],"summary":"Delete a proof rule","description":"Hard-deletes a rule. Every sign-off recorded against it cascades away with it. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the proof rule."},"required":true,"description":"UUID id of the proof rule.","name":"rule_id","in":"path"}],"responses":{"200":{"description":"The deleted rule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationRule"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this rule but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible rule has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{org_id}/departments/{dept_id}/roles/{role_id}/required-certifications":{"get":{"tags":["certifications"],"summary":"List a role's required certifications","description":"Lists the certification types required of everyone holding one department role, with each link's recertification scheme. Requires certifications:read; links whose type is outside the caller's scope are absent.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within the organization.","example":"quality-assurance"},"required":true,"description":"Department UUID id or slug, resolved within the organization.","name":"dept_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Role UUID id or slug, resolved within the department.","example":"forklift-driver"},"required":true,"description":"Role UUID id or slug, resolved within the department.","name":"role_id","in":"path"}],"responses":{"200":{"description":"The role's requirement links.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleRequiredCertificationList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department or role does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["certifications"],"summary":"Require a certification of a role","description":"Links a certification type to a department role, with a recertification scheme and an optional role-level initial due date every holder inherits. Requires certifications:write over the type's owner. The type must belong to the role's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug.","example":"acme-corporation"},"required":true,"description":"Organization UUID id or slug.","name":"org_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within the organization.","example":"quality-assurance"},"required":true,"description":"Department UUID id or slug, resolved within the organization.","name":"dept_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Role UUID id or slug, resolved within the department.","example":"forklift-driver"},"required":true,"description":"Role UUID id or slug, resolved within the department.","name":"role_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleRequiredCertificationRequest"}}}},"responses":{"201":{"description":"The created requirement link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleRequiredCertification"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold certifications:write over the type's owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department, role or certification type does not exist (or the type belongs to another organization).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The role already requires that certification type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/role-required-certifications/{link_id}":{"patch":{"tags":["certifications"],"summary":"Change a requirement's schedule","description":"Replaces the recertification scheme and role-level initial due date of one role⇄type link; the scheme, its configuration and the date always travel together — an omitted initial_due_at clears the role-level deadline. Requires certifications:write over the type's owner. Members' due dates are recomputed at read time.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the role⇄certification requirement link."},"required":true,"description":"UUID id of the role⇄certification requirement link.","name":"link_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRoleRequiredCertificationRequest"}}}},"responses":{"200":{"description":"The updated requirement link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleRequiredCertification"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this link but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible requirement link has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["certifications"],"summary":"Stop requiring a certification of a role","description":"Hard-deletes one role⇄type link, like revoking a role grant: the requirement simply stops appearing. Awards and history are untouched. Requires certifications:write over the type's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the role⇄certification requirement link."},"required":true,"description":"UUID id of the role⇄certification requirement link.","name":"link_id","in":"path"}],"responses":{"200":{"description":"The deleted requirement link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleRequiredCertification"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this link but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible requirement link has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/organizations/{organizationId}/departments/{departmentId}/roles/{roleId}/certification-completion":{"get":{"tags":["team-completion"],"summary":"A role's trainee certification completion","description":"Tallies every current holder of the role against its required certification types: who holds a current award and is not past due, per type and overall, with the named roster. Requires the caller to hold a role in this role's manager chain, or be a superuser.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","example":"6f3b34d8-3c5e-4dd9-9f4e-2b52f0d9a1c7"},"required":true,"name":"organizationId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"0b8f9a3e-97a4-4f2f-b3a3-51f2f8f0f7f4"},"required":true,"name":"departmentId","in":"path"},{"schema":{"type":"string","format":"uuid","example":"9c2f1f7e-4a35-4b8f-8d21-3f5b2a7c9d10"},"required":true,"name":"roleId","in":"path"}],"responses":{"200":{"description":"The role's aggregate completion, tallies and roster alike.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleCertificationCompletion"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller is neither in the role's manager chain nor a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, department, or role does not exist there.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/team/certification-completion":{"get":{"tags":["team-completion"],"summary":"The caller's team certification completion","description":"The aggregate completion of every role reachable walking the reporting lines down from the caller's unexpired roles, limited to roles that require at least one live certification type. Empty for a caller who manages nothing.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"One aggregate per managed role that requires certifications.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamCertificationCompletionList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-banks/{bank_id}/questions":{"get":{"tags":["quizzes"],"summary":"List a bank's questions","description":"The bank's questions in authored order, correct answers included — this is the authoring read, so it requires quizzes:read over the bank's owner. Takers never see this shape; their attempts serve questions with the answers stripped.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"fire-safety-basics"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"bank_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"}],"responses":{"200":{"description":"The bank's questions, oldest position first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question bank has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["quizzes"],"summary":"Add a question","description":"Appends one question to a bank, configured per kind: true_false with its correct_boolean; numeric with correct_number and tolerance; the rest with up to 10 options — multiple_choice (exactly one correct), multi_select (at least one, scored all-or-nothing), ordering (items in their correct order), short_answer (the accepted spellings, one or more) and matching (label/match_label pairs). The prompt is plaintext by default; prompt_format markdown renders it as Markdown, and pdf attaches a previously uploaded PDF (see the question-pdfs endpoint). Requires quizzes:write over the bank's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"fire-safety-basics"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"bank_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuizQuestionRequest"}}}},"responses":{"201":{"description":"The created question, options included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestion"}}}},"400":{"description":"The request body failed validation, does not fit the question kind, or names a PDF upload that is missing or not this bank's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this bank but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question bank has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That PDF upload is already attached to another question.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-banks/{bank_id}/question-pdfs":{"post":{"tags":["quizzes"],"summary":"Request an upload URL for a question PDF","description":"Issues a presigned URL to PUT one PDF straight to private blob storage, scoped to this bank, application/pdf and a 25 MB ceiling. Create (or update) the question with the returned pathname as prompt_pdf.pathname afterwards. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"fire-safety-basics"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"bank_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuestionPdfUploadRequest"}}}},"responses":{"201":{"description":"Where to PUT the file, and the pathname the question will point at.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuestionPdfUploadTicket"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this bank but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question bank has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The upload URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-questions/{question_id}/pdf":{"get":{"tags":["quizzes"],"summary":"Get a download URL for a question's PDF","description":"Issues a short-lived presigned GET for a pdf question's private blob — the authoring read. Requires quizzes:read over the bank's owner; takers fetch it through their attempt's question pdf endpoint instead.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the question."},"required":true,"description":"UUID id of the question.","name":"question_id","in":"path"}],"responses":{"200":{"description":"Where to fetch the PDF from, for the next five minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadTicket"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question has that id, or it has no PDF attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The download URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-banks":{"get":{"tags":["quizzes"],"summary":"List question banks","description":"Lists the question banks the caller can read, newest first, with cursor pagination. Requires quizzes:read in scope; rows outside the caller's scope are simply absent.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of question banks, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionBankList"}}}},"400":{"description":"A filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["quizzes"],"summary":"Create a question bank","description":"Creates a reusable question bank owned by an organization, or by one department within it. Banks are a shared library: any quiz of the organization may draw from them. Requires quizzes:write over the owner.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuizQuestionBankRequest"}}}},"responses":{"201":{"description":"The created question bank.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionBank"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold quizzes:write over the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, or the department within it, does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a question bank with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-banks/{bank_id}":{"get":{"tags":["quizzes"],"summary":"Read a question bank","description":"One question bank. Requires quizzes:read over the owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"fire-safety-basics"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"bank_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The question bank.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionBank"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question bank has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["quizzes"],"summary":"Update a question bank","description":"Changes a bank's slug, name or description. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"fire-safety-basics"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"bank_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuizQuestionBankRequest"}}}},"responses":{"200":{"description":"The updated question bank.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionBank"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this bank but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question bank has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a question bank with the new slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["quizzes"],"summary":"Archive a question bank","description":"Soft-deletes a question bank by setting archived_at. Its questions stop being drawn into new attempts through any quiz link; history is untouched. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"fire-safety-basics"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"bank_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The archived question bank.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestionBank"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this bank but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question bank has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-questions/{question_id}":{"patch":{"tags":["quizzes"],"summary":"Update a question","description":"Changes a question's prompt, its format, explanation or answers; sending options replaces the whole option set, and sending prompt_pdf attaches or replaces the PDF (switching prompt_format away from pdf discards it). The kind is immutable. Submitted attempts keep the grades they were given — history is never re-graded — but their answer review renders the questions as they are now. Requires quizzes:write over the bank's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the question."},"required":true,"description":"UUID id of the question.","name":"question_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuizQuestionRequest"}}}},"responses":{"200":{"description":"The updated question, options included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestion"}}}},"400":{"description":"The request body failed validation, does not fit the question's kind or format, or names a PDF upload that is missing or not this bank's.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this question but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That PDF upload is already attached to another question.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["quizzes"],"summary":"Delete a question","description":"Hard-deletes a question nobody has attempted, along with its attached PDF if any. A question presented in any attempt is kept for history and answers 409 — archive it instead. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the question."},"required":true,"description":"UUID id of the question.","name":"question_id","in":"path"}],"responses":{"200":{"description":"The deleted question, as it was (options omitted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestion"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this question but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The question has attempt history; archive it instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-questions/{question_id}/archive":{"post":{"tags":["quizzes"],"summary":"Archive a question","description":"Sets archived_at: the question stops being drawn into new papers but stays readable in past attempts. Requires quizzes:write over the bank's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the question."},"required":true,"description":"UUID id of the question.","name":"question_id","in":"path"}],"responses":{"200":{"description":"The archived question, options included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizQuestion"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this question but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible question has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/assigned":{"get":{"tags":["quiz-taking"],"summary":"List my assigned quizzes","description":"Every published quiz assigned to the caller — individually, or through any unexpired grant of an assigned role — with where they stand on each: the current and next scheduled sittings, attempts used, the open attempt to resume, and the latest result once results are visible. Needs no permission grant: being assigned is the authorization.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The caller's assigned quizzes, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignedQuizList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/attempts":{"post":{"tags":["quiz-taking"],"summary":"Start (or resume) an attempt","description":"Starts an attempt of an assigned, published quiz that is takeable right now — inside an open sitting for live quizzes, any time for async ones. The paper is drawn per the quiz's selection mode, persisted, and served with the answers stripped. An attempt already in progress is returned instead of starting another (200, not 201). Refused outside a sitting, or once max_attempts is used up — counted within the current sitting for live quizzes, per quiz for async. Needs no permission grant: being assigned is the authorization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The attempt already in progress, resumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttemptDetail"}}}},"201":{"description":"The freshly started attempt and its paper.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttemptDetail"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No quiz assigned to the caller has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"No sitting is open, no attempts remain, or the paper is empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["quizzes"],"summary":"List a quiz's attempts","description":"Every attempt of the quiz, newest first, scores included — the results view for holders of quizzes:read over the quiz's owner. Expired in-progress attempts are finalized (auto-submitted as-is) before listing, so no background job is needed.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of attempts, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttemptList"}}}},"400":{"description":"The cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-attempts/{attempt_id}/questions/{question_id}/pdf":{"get":{"tags":["quiz-taking"],"summary":"Get a download URL for a presented question's PDF","description":"Issues a short-lived presigned GET for the PDF of a pdf question this attempt presented — while sitting the attempt and in answer review alike. Readable by the taker themselves, and by holders of quizzes:read over the quiz. Carries no answers.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the attempt."},"required":true,"description":"UUID id of the attempt.","name":"attempt_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the question."},"required":true,"description":"UUID id of the question.","name":"question_id","in":"path"}],"responses":{"200":{"description":"Where to fetch the PDF from, for the next five minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadTicket"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible attempt has that id, the question is not on its paper, or it has no PDF.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The download URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-attempts/{attempt_id}/review":{"get":{"tags":["quiz-taking"],"summary":"Review an attempt's answers","description":"The attempt's paper with correct answers, the taker's responses and per-question grades. Takers see it only once the attempt is submitted, its results are visible, and the quiz is configured to show correct answers; holders of quizzes:read over the quiz always may. Questions render as they are *now* — an edit after the sitting changes the review, never the recorded grades.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the attempt."},"required":true,"description":"UUID id of the attempt.","name":"attempt_id","in":"path"}],"responses":{"200":{"description":"The graded paper, answers included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttemptReview"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Review is not (or not yet) available to the taker for this quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible attempt has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-attempts/{attempt_id}":{"get":{"tags":["quiz-taking"],"summary":"Read an attempt","description":"The attempt and its paper, answers stripped — the taking and resume view. Readable by the taker themselves, and by holders of quizzes:read over the quiz. An attempt past its deadline is finalized (auto-submitted as-is) by this read. Held-back results are nulled for the taker until released; permission holders always see them.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the attempt."},"required":true,"description":"UUID id of the attempt.","name":"attempt_id","in":"path"}],"responses":{"200":{"description":"The attempt and its paper.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttemptDetail"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible attempt has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-attempts/{attempt_id}/answers":{"post":{"tags":["quiz-taking"],"summary":"Answer one question","description":"Saves (or replaces) the taker's answer to one presented question, in the shape its kind takes: response_boolean (true_false), selected_option_ids (multiple_choice takes exactly one id; multi_select the chosen ids; ordering every presented item in the taker's arrangement), response_text (short_answer), response_number (numeric) or response_matches (matching; partial pair lists are fine). Only the taker may answer, only while the attempt is in progress and its deadline has not passed. Answers are not graded here; grading happens at submit.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the attempt."},"required":true,"description":"UUID id of the attempt.","name":"attempt_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnswerQuizQuestionRequest"}}}},"responses":{"200":{"description":"The attempt with the saved answer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttemptDetail"}}}},"400":{"description":"The answer does not fit the question's kind or options.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible attempt has that id, or the question is not on its paper.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The attempt is already submitted, or its deadline has passed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-attempts/{attempt_id}/submit":{"post":{"tags":["quiz-taking"],"summary":"Submit an attempt","description":"Grades the attempt server-side, scores it against the quiz's pass mark, and reports it into the taker's learning record for the quiz. Unanswered questions score zero. Submitting twice is safe — the second call returns the already-graded attempt. Score and pass/fail travel back only when the quiz's rules make results visible.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the attempt."},"required":true,"description":"UUID id of the attempt.","name":"attempt_id","in":"path"}],"responses":{"200":{"description":"The submitted attempt; results nulled while withheld.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAttempt"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible attempt has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/schedules":{"post":{"tags":["quizzes"],"summary":"Schedule a sitting","description":"Adds one scheduled sitting of the quiz — live quizzes are takeable only inside an open sitting, and max_attempts counts per sitting, so recurring runs (say, yearly recertification) are just more sittings. Sittings of one quiz must not overlap. Sittings of async quizzes are accepted but ignored. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScheduledQuizRequest"}}}},"responses":{"201":{"description":"The scheduled sitting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledQuiz"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The sitting would overlap another sitting of the quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-schedules/{schedule_id}":{"patch":{"tags":["quizzes"],"summary":"Update a sitting","description":"Changes a sitting's label or window. Attempts already sat in it keep their recorded deadlines and grades; moving the close changes when their held-back results release. Requires quizzes:write over the quiz's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the scheduled sitting."},"required":true,"description":"UUID id of the scheduled sitting.","name":"schedule_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScheduledQuizRequest"}}}},"responses":{"200":{"description":"The updated sitting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledQuiz"}}}},"400":{"description":"The request body failed validation, or the merged window is inverted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this sitting but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible sitting has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The sitting would overlap another sitting of the quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["quizzes"],"summary":"Delete a sitting","description":"Removes a sitting nobody has sat. A sitting with attempts is history and answers 409 — move its window instead. Requires quizzes:write over the quiz's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the scheduled sitting."},"required":true,"description":"UUID id of the scheduled sitting.","name":"schedule_id","in":"path"}],"responses":{"200":{"description":"The deleted sitting, as it was.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduledQuiz"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this sitting but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible sitting has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The sitting has attempts; it is history and cannot be deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/assignments":{"get":{"tags":["quizzes"],"summary":"List a quiz's assignments","description":"Who may see and take the quiz: assigned department roles (every holder) and individually assigned members. Requires quizzes:read over the quiz's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The quiz's assignments, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAssignmentList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["quizzes"],"summary":"Assign a quiz","description":"Assigns the quiz to a department role (every holder can take it) or to one directory member — exactly one of role_id / user_id. Both must belong to the quiz's own organization. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuizAssignmentRequest"}}}},"responses":{"201":{"description":"The created assignment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAssignment"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The quiz, role, or member does not exist in the quiz's organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The quiz is already assigned to that role or member.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quiz-assignments/{assignment_id}":{"delete":{"tags":["quizzes"],"summary":"Remove an assignment","description":"Unassigns a role or member from the quiz. Attempts already made are history and stay. Requires quizzes:write over the quiz's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the assignment."},"required":true,"description":"UUID id of the assignment.","name":"assignment_id","in":"path"}],"responses":{"200":{"description":"The removed assignment, as it was (display names omitted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizAssignment"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible assignment has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/banks":{"put":{"tags":["quizzes"],"summary":"Set a quiz's bank links","description":"Replaces the quiz's bank links; array order is presentation order, and draw_count bounds what a random_draw quiz pulls from each bank (omit for all). Banks must belong to the quiz's own organization. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetQuizBankLinksRequest"}}}},"responses":{"200":{"description":"The quiz with its new bank links.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizDetail"}}}},"400":{"description":"The body failed validation, or a bank is not in the quiz's organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/subjects":{"put":{"tags":["quizzes"],"summary":"Set a quiz's subject matters","description":"Replaces the quiz's subject-matter tags — the subjects each taker's learning record is filed under. Subjects must belong to the quiz's own organization. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetQuizSubjectsRequest"}}}},"responses":{"200":{"description":"The quiz with its new subject tags.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizDetail"}}}},"400":{"description":"The body failed validation, or a subject is not in the quiz's organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/publish":{"post":{"tags":["quizzes"],"summary":"Publish a quiz","description":"Draft → published: assignees can now see the quiz, and sit it inside one of its scheduled sittings (live) or at any time (async). Refused while the linked banks hold no live questions, or while a live quiz has no sitting scheduled. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The published quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quiz"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The quiz would present no questions, or a live quiz has no sitting.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}/release-results":{"post":{"tags":["quizzes"],"summary":"Release held-back results","description":"Stamps results_released_at, making every taker's results visible — the manual release for quizzes that neither show results immediately nor have a closing window. Expired in-progress attempts are finalized first. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The quiz, with results released.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quiz"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes/{quiz_id}":{"delete":{"tags":["quizzes"],"summary":"Archive a quiz","description":"Soft-deletes a quiz by setting archived_at: it disappears from assignees and cannot be sat; attempts and their learning records stay readable. Requires quizzes:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The archived quiz.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Quiz"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["quizzes"],"summary":"Read a quiz","description":"One quiz with its bank links, subject tags and scheduled sittings — the admin view. Requires quizzes:read over the owner. Takers read their assigned view at /quizzes/assigned.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The quiz, bank links and subjects included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizDetail"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["quizzes"],"summary":"Update a quiz","description":"Changes a quiz's configuration. Published quizzes stay editable — attempts are graded at submit time, so history never re-grades — but selection and limit changes apply to future attempts immediately. Sittings are managed at /quizzes/{quiz_id}/schedules. Requires quizzes:write over the owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"annual-fire-safety"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"quiz_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuizRequest"}}}},"responses":{"200":{"description":"The updated quiz, bank links and subjects included.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizDetail"}}}},"400":{"description":"The request body failed validation, or the window rules were broken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this quiz but does not hold quizzes:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible quiz has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a quiz with the new slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/quizzes":{"get":{"tags":["quizzes"],"summary":"List quizzes","description":"Lists the quizzes the caller can administer or read, newest first, with cursor pagination. Requires quizzes:read in scope; rows outside the caller's scope are simply absent. Takers list what is assigned to them at /quizzes/assigned instead.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of quizzes, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizList"}}}},"400":{"description":"A filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["quizzes"],"summary":"Create a quiz","description":"Creates a quiz in draft, owned by an organization or one department within it. Question banks of the owning organization can be linked from the start via bank_links (or later via PUT /quizzes/{quiz_id}/banks); tag subjects and assign takers next, then publish. live quizzes are sat inside scheduled sittings (POST /quizzes/{quiz_id}/schedules) — the same quiz can be scheduled again and again; async ones are takeable whenever published. Requires quizzes:write over the owner.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuizRequest"}}}},"responses":{"201":{"description":"The created quiz, in draft, with any bank links it was created with.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizDetail"}}}},"400":{"description":"The request body failed validation, or a requested bank is not in the owning organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold quizzes:write over the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, or the department within it, does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a quiz with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/mine":{"get":{"tags":["journeys"],"summary":"List my journeys with progress","description":"Every unarchived journey whose audience covers the caller — through a held role, a department they hold a grant in (or one above it), or organization membership — with every level's derived state. Needs no permission grant: the audience is the gate. Locked levels are presentation only; nothing is enforced server-side.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The caller's journeys, each with derived progress.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyJourneys"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/progress":{"get":{"tags":["journeys"],"summary":"Get my progress on one journey","description":"The journey's level graph as it stands for the caller: which levels are complete, unlocked or locked, and where every certification stands. Readable by anyone the journey's audience covers, and by holders of certifications:read over its owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The journey as it stands for the caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyProgress"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/attachment-uploads":{"post":{"tags":["journeys"],"summary":"Request an upload URL for a journey attachment","description":"Issues a presigned URL to PUT one file of any type straight to private blob storage, scoped to this journey, the declared content type and a 500 MB ceiling. Attach it with POST /journeys/{journey_id}/attachments and the returned pathname afterwards. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDescriptionUploadRequest"}}}},"responses":{"201":{"description":"Where to PUT the file, and the pathname the attachment will point at.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionUploadTicket"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey matches the path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The upload URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/attachments":{"post":{"tags":["journeys"],"summary":"Attach an uploaded file to a journey","description":"Creates the attachment at the end of the authored order, pointing at a pathname an upload ticket was issued for (after PUTting the file there). The content type is read back from the store, never from the request. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDescriptionAttachmentRequest"}}}},"responses":{"201":{"description":"The created attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionAttachment"}}}},"400":{"description":"The body failed validation, or the pathname was not uploaded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey matches the path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That upload is already attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/attachments/{attachment_id}/download":{"get":{"tags":["journeys"],"summary":"Get a download URL for a journey attachment","description":"Issues a short-lived presigned GET for the private attachment. Readable by anyone the journey's audience covers, and by holders of certifications:read over its owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"Where to fetch the file from, for the next five minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadTicket"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The download URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/attachments/{attachment_id}":{"patch":{"tags":["journeys"],"summary":"Relabel or reorder a journey attachment","description":"Changes the attachment's label (null or empty clears it back to the file name) or its slot in the authored order. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDescriptionAttachmentRequest"}}}},"responses":{"200":{"description":"The updated attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionAttachment"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["journeys"],"summary":"Delete a journey attachment","description":"Removes the attachment; the blob it pointed at is deleted best-effort. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"204":{"description":"The attachment is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journey-levels/{level_id}/attachment-uploads":{"post":{"tags":["journeys"],"summary":"Request an upload URL for a level attachment","description":"Issues a presigned URL to PUT one file of any type straight to private blob storage, scoped to this level, the declared content type and a 500 MB ceiling. Attach it with POST /journey-levels/{level_id}/attachments and the returned pathname afterwards. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDescriptionUploadRequest"}}}},"responses":{"201":{"description":"Where to PUT the file, and the pathname the attachment will point at.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionUploadTicket"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this level but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level matches the path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The upload URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journey-levels/{level_id}/attachments":{"post":{"tags":["journeys"],"summary":"Attach an uploaded file to a level","description":"Creates the attachment at the end of the authored order, pointing at a pathname an upload ticket was issued for (after PUTting the file there). The content type is read back from the store, never from the request. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDescriptionAttachmentRequest"}}}},"responses":{"201":{"description":"The created attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionAttachment"}}}},"400":{"description":"The body failed validation, or the pathname was not uploaded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this level but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level matches the path.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That upload is already attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journey-levels/{level_id}/attachments/{attachment_id}/download":{"get":{"tags":["journeys"],"summary":"Get a download URL for a level attachment","description":"Issues a short-lived presigned GET for the private attachment. Readable by anyone the level's journey's audience covers, and by holders of certifications:read over its owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"}],"responses":{"200":{"description":"Where to fetch the file from, for the next five minutes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadTicket"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The download URL could not be signed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journey-levels/{level_id}/attachments/{attachment_id}":{"patch":{"tags":["journeys"],"summary":"Relabel or reorder a level attachment","description":"Changes the attachment's label (null or empty clears it back to the file name) or its slot in the authored order. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDescriptionAttachmentRequest"}}}},"responses":{"200":{"description":"The updated attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DescriptionAttachment"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this level but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["journeys"],"summary":"Delete a level attachment","description":"Removes the attachment; the blob it pointed at is deleted best-effort. Requires certifications:write.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the attachment."},"required":true,"description":"UUID id of the attachment.","name":"attachment_id","in":"path"}],"responses":{"204":{"description":"The attachment is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this level but lacks certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level matches the path, or it has no such attachment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/levels":{"post":{"tags":["journeys"],"summary":"Add a level to a journey","description":"Creates a level at the end of the authored order, with its prerequisite levels and certification contents in one call. A certification type may appear in at most one level per journey. Requires certifications:write over the journey's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJourneyLevelRequest"}}}},"responses":{"201":{"description":"The created level, with its prerequisites and contents.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyLevel"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug, or a prerequisite level or certification type does not belong to it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A certification type is already placed elsewhere in this journey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journey-levels/{level_id}":{"patch":{"tags":["journeys"],"summary":"Update a level","description":"Renames or re-describes a level; depends_on_level_ids replaces its whole prerequisite set (rejected when the edges would close a cycle), certification_type_ids its whole contents. Requires certifications:write over the journey's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateJourneyLevelRequest"}}}},"responses":{"200":{"description":"The updated level, with its prerequisites and contents.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyLevel"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level has that id, or a prerequisite level or certification type does not belong to its journey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The prerequisites would form a cycle, or a certification type is already placed elsewhere in this journey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["journeys"],"summary":"Delete a level","description":"Removes the level, its edges and its contents; levels that depended on it simply lose that prerequisite. Requires certifications:write over the journey's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the level."},"required":true,"description":"UUID id of the level.","name":"level_id","in":"path"}],"responses":{"204":{"description":"The level is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible level has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/assignments":{"post":{"tags":["journeys"],"summary":"Assign a journey's audience","description":"Presents the journey to holders of a role, to everyone with a role grant in a department (or one nested beneath it), or to every member of the organization. The audience is presentation only — it never adds a certification requirement. Requires certifications:write over the journey's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJourneyAssignmentRequest"}}}},"responses":{"201":{"description":"The created assignment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyAssignment"}}}},"400":{"description":"The request body failed validation, or the kind/id pairing is off.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug, or the role or department is not in the journey's organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The journey already has that audience row.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/assignments/{assignment_id}":{"delete":{"tags":["journeys"],"summary":"Remove one audience row","description":"Withdraws the journey from that role, department or organization audience. Requires certifications:write over the journey's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","format":"uuid","description":"UUID id of the assignment."},"required":true,"description":"UUID id of the assignment.","name":"assignment_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"204":{"description":"The assignment is gone."},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug, or no such assignment on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys":{"get":{"tags":["journeys"],"summary":"List learner's journeys","description":"Lists the journeys the caller can administer, newest first, with cursor pagination. Requires certifications:read in scope; rows outside the caller's scope are simply absent. Trainees list what is assigned to them at /journeys/mine instead.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of journeys, and the cursor for the next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyList"}}}},"400":{"description":"A filter did not resolve, or the cursor is malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["journeys"],"summary":"Create a learner's journey","description":"Creates a journey, owned by an organization or one department within it. A journey arranges existing certification types into levels with prerequisite edges — a presentation overlay that never adds a requirement. Add levels and assign the audience next. Requires certifications:write over the owner.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJourneyRequest"}}}},"responses":{"201":{"description":"The created journey, with no levels yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Journey"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold certifications:write over the owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The organization, or the department within it, does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a journey with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}":{"get":{"tags":["journeys"],"summary":"Get a journey with its structure","description":"The journey, every level with its prerequisites and certification contents, and the audience — the authoring read. Requires certifications:read over the owner; trainees read their own derived view at /journeys/{journey_id}/progress instead.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The journey, its levels and its audience.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JourneyDetail"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["journeys"],"summary":"Update a journey","description":"Renames, re-slugs or re-describes a journey. File attachments are managed through the /journeys/{journey_id}/attachments endpoints. Requires certifications:write over the owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateJourneyRequest"}}}},"responses":{"200":{"description":"The updated journey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Journey"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a journey with the new slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/journeys/{journey_id}/archive":{"post":{"tags":["journeys"],"summary":"Archive a journey","description":"Retires the journey: it stays readable to administrators but leaves every trainee's journey list. Idempotent. Requires certifications:write over the owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"new-hire-path"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"journey_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The archived journey.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Journey"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this journey but does not hold certifications:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible journey has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers":{"get":{"tags":["external-lms"],"summary":"List external LMS servers","description":"Lists one organization's external LMS servers, alphabetically. Requires external-lms:read over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug whose catalogue to list. Required."},"required":true,"description":"Organization UUID id or slug whose catalogue to list. Required.","name":"org","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"}],"responses":{"200":{"description":"The organization's servers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsServerList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible organization has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["external-lms"],"summary":"Add an external LMS server","description":"Adds a server to an organization's catalogue. Descriptive metadata only — the LRS never connects to it. Requires external-lms:write over the organization.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExternalLmsServerRequest"}}}},"responses":{"201":{"description":"The created server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsServer"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold external-lms:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible organization has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The organization already has a server with that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers/{server_id}/grades/import":{"post":{"tags":["external-lms"],"summary":"Import course grades from a CSV file","description":"Bulk variant of the grades import: one uploaded gradebook CSV instead of a JSON batch, with the same one-outcome-per-row semantics — malformed cells fail their row, not the file, so a fixed export can simply be uploaded again (already-imported rows come back as duplicate). Requires records:write over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/ExternalGradesImportForm"}}}},"responses":{"200":{"description":"One outcome per data row, in file order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestExternalGradesResult"}}}},"400":{"description":"The file is not parseable CSV, a required column is missing from the header, the file has no (or too many) data rows, or the server is archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold records:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"The file is larger than 1 MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers/{server_id}/grades":{"post":{"tags":["external-lms"],"summary":"Import course grades","description":"Imports a batch of grades from this server. Each created grade becomes a completed learning record (owned by the organization) carrying the score as evidence, plus a queryable grade row. Rows succeed or fail one by one — re-submitting a batch after fixing a mapping is the normal workflow; already-imported rows come back as duplicate. Learners are identified by external_user_id through the server's user mappings, or directly by user_id. Requires records:write over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestExternalGradesRequest"}}}},"responses":{"200":{"description":"One outcome per submitted row, in row order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestExternalGradesResult"}}}},"400":{"description":"The request body failed validation, or the server is archived.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold records:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["external-lms"],"summary":"List recently imported grades","description":"The server's most recently imported grades, newest first. Requires external-lms:read over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"}],"responses":{"200":{"description":"The grades, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalCourseGradeList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers/{server_id}/user-mappings":{"get":{"tags":["external-lms"],"summary":"List a server's user mappings","description":"Lists how the server's external user identities resolve to directory members. Requires external-lms:read over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The server's mappings, by external user id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsUserMappingList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"put":{"tags":["external-lms"],"summary":"Create or repoint a user mapping","description":"Maps one external user identity to a directory member; writing an existing identity repoints it. Grades already imported keep the member they resolved to at import time. Requires external-lms:write over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertExternalLmsUserMappingRequest"}}}},"responses":{"200":{"description":"The server's mappings after the write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsUserMappingList"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold external-lms:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug, or the user is not in the organization's directory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers/{server_id}/user-mappings/{external_user_id}":{"delete":{"tags":["external-lms"],"summary":"Delete a user mapping","description":"Unmaps one external user identity; grades already imported through it are untouched. Requires external-lms:write over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"The LMS's own identity of the learner, URL-encoded.","example":"jdoe"},"required":true,"description":"The LMS's own identity of the learner, URL-encoded.","name":"external_user_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The server's mappings after the delete.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsUserMappingList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold external-lms:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug, or no mapping has that identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers/{server_id}/courses":{"get":{"tags":["external-lms"],"summary":"List a server's courses","description":"Lists one server's catalogue courses, alphabetically. Requires external-lms:read over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"}],"responses":{"200":{"description":"The server's courses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsCourseList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["external-lms"],"summary":"Add a course to a server's catalogue","description":"Adds one course, keyed by the LMS's own course identifier — the id grades arrive under. Requires external-lms:write over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExternalLmsCourseRequest"}}}},"responses":{"201":{"description":"The created course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsCourse"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold external-lms:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug, or the subject matter is not the organization's own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The server already catalogues a course with that external id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/servers/{server_id}":{"get":{"tags":["external-lms"],"summary":"Read one external LMS server","description":"Requires external-lms:read over the server's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsServer"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["external-lms"],"summary":"Update an external LMS server","description":"Changes a server's name, description, URL or kind. The slug is immutable. Requires external-lms:write over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExternalLmsServerRequest"}}}},"responses":{"200":{"description":"The updated server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsServer"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this server but does not hold external-lms:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["external-lms"],"summary":"Archive an external LMS server","description":"Soft-deletes a server by setting archived_at. Its courses, mappings and imported grades stay readable, but the server stops taking grades. Requires external-lms:write over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","example":"moodle-eu"},"required":true,"description":"UUID id, or a slug resolved within the organization given by the org query parameter.","name":"server_id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"}],"responses":{"200":{"description":"The archived server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsServer"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this server but does not hold external-lms:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible server has that id or slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/external-lms/courses/{course_id}":{"get":{"tags":["external-lms"],"summary":"Read one catalogue course","description":"Requires external-lms:read over the course's organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the catalogue course."},"required":true,"description":"UUID id of the catalogue course.","name":"course_id","in":"path"}],"responses":{"200":{"description":"The course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsCourse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible course has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["external-lms"],"summary":"Update a catalogue course","description":"Changes a course's name, description, URL, pass mark or subject tag. The external course id is immutable — grades key on it. Requires external-lms:write over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the catalogue course."},"required":true,"description":"UUID id of the catalogue course.","name":"course_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateExternalLmsCourseRequest"}}}},"responses":{"200":{"description":"The updated course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsCourse"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this course but does not hold external-lms:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible course has that id, or the subject matter is not the organization's own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["external-lms"],"summary":"Archive a catalogue course","description":"Soft-deletes a course by setting archived_at. Imported grades stay, but the course stops taking new ones. Requires external-lms:write over the organization.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the catalogue course."},"required":true,"description":"UUID id of the catalogue course.","name":"course_id","in":"path"}],"responses":{"200":{"description":"The archived course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalLmsCourse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller can read this course but does not hold external-lms:write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible course has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-courses/available":{"get":{"tags":["scorm-taking"],"summary":"List the courses the caller can take","description":"Lists the live SCORM courses the caller may take, each with their own attempt state: organization-owned courses of every organization whose directory names them, and department-owned courses of departments they hold a role in. Needs no permission grant: the course's audience is the authorization.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The caller's available courses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailableScormCourseList"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-courses/{course_id}/attempts":{"post":{"tags":["scorm-taking"],"summary":"Launch a course (start or resume an attempt)","description":"Prepares a session of the caller's attempt: resumes the attempt in progress (its saved CMI data seeds the runtime, and its accumulated time rolls forward as the new session's base), or starts attempt n+1 when none is open — immediately with restart: true, which closes the open attempt first. The response carries everything the in-page runtime needs, including the signed content URL the module's iframe loads. Needs no permission grant: the course's audience is the authorization — organization membership, narrowed to the owning department's role holders for department-owned courses.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the SCORM course."},"required":true,"description":"UUID id of the SCORM course.","name":"course_id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaunchScormCourseRequest"}}}},"responses":{"200":{"description":"The attempt already in progress, resumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormLaunch"}}}},"201":{"description":"A freshly started attempt.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormLaunch"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No course available to the caller has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"A concurrent launch is already starting an attempt; retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["scorm"],"summary":"List a course's attempts","description":"Lists every learner's attempts of one course, newest first, one page at a time — each joined with the learner's current profile. Requires scorm:read over the course's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the SCORM course."},"required":true,"description":"UUID id of the SCORM course.","name":"course_id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"Page size, 1-200. Defaults to 50."},"required":false,"description":"Page size, 1-200. Defaults to 50.","name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Opaque cursor from a previous page's next_cursor."},"required":false,"description":"Opaque cursor from a previous page's next_cursor.","name":"cursor","in":"query"}],"responses":{"200":{"description":"One page of the course's attempts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormCourseAttemptList"}}}},"400":{"description":"The cursor is not one this API issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible course has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-attempts/{attempt_id}":{"put":{"tags":["scorm-taking"],"summary":"Commit an attempt's CMI data","description":"Saves what the module has reported: the full CMI element map replaces the attempt's saved copy, and the summary the row carries — completion, success, scores, location, accumulated time — is re-derived from it server-side. The first commit that reports the attempt complete also files the result into the learner's records. The runtime calls this on every LMSCommit/Commit and on LMSFinish/Terminate. Only the attempt's own learner may commit; not their attempt is indistinguishable from a missing one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the attempt."},"required":true,"description":"UUID id of the attempt.","name":"attempt_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommitScormAttemptRequest"}}}},"responses":{"200":{"description":"The attempt with its freshly derived summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormAttempt"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No attempt of the caller's has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The attempt is closed; launch the course again for a fresh one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-content/{ticket}/{asset_path}":{"get":{"tags":["scorm-taking"],"summary":"Serve a file from a course package","description":"Serves one file out of a SCORM course's package zip: the launch page the player's iframe loads, and every asset it references by relative URL. The signed ticket segment is the authorization — POST /scorm-courses/{course_id}/attempts mints it into content_url after checking the caller may take the course — so no bearer token is required (module asset requests cannot carry one). Tickets are scoped to one course and expire with the session.","security":[],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Signed, course-scoped content ticket — the launch response's content_url carries it."},"required":true,"description":"Signed, course-scoped content ticket — the launch response's content_url carries it.","name":"ticket","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Package-relative path of the file, spanning multiple path segments (the launch page's relative asset URLs resolve here on their own)."},"required":true,"description":"Package-relative path of the file, spanning multiple path segments (the launch page's relative asset URLs resolve here on their own).","name":"asset_path","in":"path"}],"responses":{"200":{"description":"The file's bytes, served with the package entry's own content type."},"400":{"description":"The asset path is missing or malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The content ticket is invalid or has expired; relaunch the course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The course is gone, or no file at that path exists in its package.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The course content could not be served.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"The course package could not be read from storage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-courses/uploads":{"post":{"tags":["scorm"],"summary":"Mint an upload ticket for a SCORM package","description":"Issues a presigned upload URL for one SCORM package (.zip), bypassing the request-body limit by sending the bytes straight to the private blob store. The signed ticket pins a server-chosen pathname, zip content types and the package size cap. The staged upload becomes a course only through POST /scorm-courses, which validates the package first. Requires scorm:write over the intended owner — the organization, or the owning department within it when one is named.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormPackageUploadRequest"}}}},"responses":{"201":{"description":"The presigned upload ticket.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormPackageUploadTicket"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold scorm:write over the intended owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible organization has that id or slug, or the department is not one of the organization's own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The upload ticket could not be issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-courses":{"get":{"tags":["scorm"],"summary":"List SCORM courses","description":"Lists the hosted SCORM courses the caller's scorm:read scope covers, newest first, optionally filtered to one organization or one owning department within it. Learners list what they can take through GET /scorm-courses/available instead.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","example":"acme-corporation"},"required":false,"description":"Organization UUID id or slug to filter by (and to resolve slug refs in).","name":"org","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Department UUID id or slug, resolved within org (which is then required).","example":"quality-assurance"},"required":false,"description":"Department UUID id or slug, resolved within org (which is then required).","name":"department","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Include archived rows. Defaults to false."},"required":false,"description":"Include archived rows. Defaults to false.","name":"include_archived","in":"query"}],"responses":{"200":{"description":"The visible courses.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormCourseList"}}}},"400":{"description":"The department filter was given without an org filter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible organization has that id or slug, or the department is not one of the organization's own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["scorm"],"summary":"Register an uploaded package as a course","description":"Completes a package upload: validates the staged zip (it must contain an imsmanifest.xml whose launch resource exists in the archive), captures the launch path, SCORM version, manifest objectives and mastery score, and creates the course — owned by the organization, or by the owning department named within it. Requires scorm:write over that owner. An invalid package is rejected and its staged blob deleted.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScormCourseRequest"}}}},"responses":{"201":{"description":"The registered course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormCourse"}}}},"400":{"description":"The request body failed validation, no upload exists at the pathname, or the package is not a runnable SCORM module.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold scorm:write over the intended owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible organization has that id or slug, or the department or subject matter is not the organization's own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"That uploaded package is already registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/scorm-courses/{course_id}":{"get":{"tags":["scorm"],"summary":"Read one SCORM course","description":"Reads one course. Requires scorm:read over the course's owner — its owning department when it has one, its organization otherwise.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the SCORM course."},"required":true,"description":"UUID id of the SCORM course.","name":"course_id","in":"path"}],"responses":{"200":{"description":"The course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormCourse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible course has that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["scorm"],"summary":"Edit or archive a course","description":"Changes a course's name, description or subject matter, or archives/restores it. The package itself is immutable — new content is a new course. Attempts are history, so there is no delete; archiving hides the course from learners. Requires scorm:write over the course's owner.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"UUID id of the SCORM course."},"required":true,"description":"UUID id of the SCORM course.","name":"course_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScormCourseRequest"}}}},"responses":{"200":{"description":"The updated course.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScormCourse"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller does not hold scorm:write over the organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No visible course has that id, or the subject matter is not the organization's own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The database could not be reached, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/notifications/certification-requirements":{"post":{"tags":["notifications"],"summary":"Send a certification requirements notification email","description":"Builds a digest of certification requirements grouped into due windows (overdue, next 10/30/60 days), renders it to HTML + plaintext, validates it against the @schemavaults/send-email request schema, and submits it to the configured transactional mail server. `audience: trainee` covers the user's own requirements; `audience: manager` covers the trainees holding roles the user manages. Superusers only — this is the manual trigger behind the admin notifications page; scheduled sending will reuse the same pipeline. Use `dry_run` to validate without sending.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendCertificationNotificationRequest"}}}},"responses":{"200":{"description":"The digest was sent, validated (dry_run), or skipped as empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificationNotificationResult"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The caller is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No recipient email address could be resolved for the user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The digest could not be built or rendered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"The mail server rejected the message or could not be reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/branding":{"get":{"tags":["branding"],"summary":"Read the deployment branding","description":"The long-form and short names and the logo this deployment shows in place of the Botree defaults. Null fields mean the default is in use.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The current branding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentBranding"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The deployment branding could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["branding"],"summary":"Set the deployment's names","description":"Sets the long-form name shown in the footer, page titles and notification emails, and the short name shown as the dashboard wordmark and top bar title. Either falls back to the other when only one is set; send null or an empty string to clear one, and omit a field to leave it unchanged. The footer keeps a \"Powered by Botree LRS\" credit while a custom name or logo is set. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeploymentBrandingRequest"}}}},"responses":{"200":{"description":"The updated branding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentBranding"}}}},"400":{"description":"The request body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The deployment branding could not be written to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/branding/logo":{"get":{"tags":["branding"],"summary":"Serve the deployment logo","description":"The uploaded logo's bytes, with its content type. Public — the header and footer load it as an image — and 404 while the Botree artwork is in use. The pages append a version query for cache-busting; it is ignored here.","security":[],"responses":{"200":{"description":"The image, served with the upload's content type."},"404":{"description":"No logo has been uploaded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The deployment branding could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"The logo could not be read from storage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["branding"],"summary":"Upload the deployment logo","description":"Replaces the logo shown in the header and footer with the image in the file part: PNG, JPEG, WebP, GIF or SVG, at most 2 MB. Square artwork fits best. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/DeploymentBrandingLogoForm"}}}},"responses":{"200":{"description":"The updated branding, its logo URL pointing at the new image.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentBranding"}}}},"400":{"description":"The file part is missing, not an accepted image type, or too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The image could not be stored, or the write failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"File storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["branding"],"summary":"Remove the deployment logo","description":"Restores the Botree artwork in the header and footer. Requires a superuser's access token.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The updated branding, with no logo.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentBranding"}}}},"401":{"description":"The access token is missing or invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"The access token's subject is not a superuser.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"The deployment branding could not be written to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"webhooks":{}}