> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safeonlineexam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Root-Account Administrator Routes — Safe Online Exam API

> Reference for /api/admin: account summary, course connection and reset, password reveal, quit-password rotation, SEB toggles, and preset rollout batches.

Safe Online Exam exposes all root-account administrator operations under the `/api/admin` prefix. This page documents every route available through the Canvas-embedded school dashboard — from reading the account summary and browsing connected courses, through controlled password reveals and course resets, to managing reusable school tool presets and processing rollout batches.

<Note>
  Every route under `/api/admin` requires a **verified root-account administrator principal**. That principal is established only after a successful LTI 1.3 launch that presents both a signed LTI Administrator role and Canvas's signed root-account-admin substitution value. The numeric Canvas user ID and root-account ID are taken from the validated token; they cannot be supplied through query parameters or request bodies.
</Note>

<Note>
  **HMAC action token for mutations.** All state-changing routes (marked `†`) additionally require a short-lived HMAC action token bound to the LTI subject, Canvas user ID, root-account ID, deployment ID, and current Express session. The token is issued by the server after a successful LTI launch and must accompany every mutation request. Requests that lack a valid token are rejected before reaching application logic.
</Note>

***

## Summary and Course Listing

### `GET /api/admin/summary`

Returns a lightweight dashboard summary: Canvas account metadata, administrator permissions, the active operational enrollment term, configured-course totals, and tool-preset assignment counts. This endpoint triggers background course-status reconciliation (bounded to `ADMIN_RECONCILE_BATCH_SIZE = 12` courses) and a one-time legacy connection backfill on first access.

**Auth:** Verified root-account administrator. No action token required.

```json theme={null}
{
  "success": true,
  "account": { "id": "1", "name": "Example University" },
  "permissions": { "manage_courses": true },
  "operationalTerm": { "id": "42", "name": "Fall 2024" },
  "summary": {
    "courseCount": 18,
    "enabledAssessmentCount": 54,
    "configuredCourseCount": 18,
    "toolPresetCount": 3,
    "pendingPresetAssignmentCount": 2,
    "failedPresetAssignmentCount": 0
  }
}
```

***

### `GET /api/admin/courses`

Pages through connected courses for the active operational term. Supports cursor-based pagination, optional free-text search, and an `includePast` flag to include concluded courses. The response also includes the resolved operational term.

**Auth:** Verified root-account administrator. No action token required.

<ParamField query="search" type="string">
  Free-text filter applied to course name. Trimmed to 120 characters; extra whitespace is collapsed.
</ParamField>

<ParamField query="cursor" type="string">
  Base64URL-encoded pagination cursor from a previous response's `nextCursor` field.
</ParamField>

<ParamField query="limit" type="number">
  Page size, 1–50. Defaults to `ADMIN_PAGE_SIZE = 25`.
</ParamField>

<ParamField query="includePast" type="boolean">
  Pass `"true"` to include concluded courses. Defaults to `false`.
</ParamField>

```json theme={null}
{
  "success": true,
  "operationalTerm": { "id": "42", "name": "Fall 2024" },
  "includePast": false,
  "courses": [
    {
      "id": "456",
      "name": "Introduction to Biology",
      "courseCode": "BIO101",
      "assessmentCount": 5,
      "enabledAssessmentCount": 3,
      "issueCount": 0,
      "concluded": false,
      "lastRefreshedAt": "2024-09-01T10:00:00.000Z"
    }
  ],
  "nextCursor": "eyJuYW1lIjoiSW50cm8..."
}
```

***

### `GET /api/admin/courses/:courseId`

Returns full detail for one connected course, including its assessments, local setup state, and the list of school tool preset IDs currently assigned to it.

**Auth:** Verified root-account administrator. The server validates that `:courseId` falls within the administrator's Canvas root account.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

```json theme={null}
{
  "success": true,
  "course": {
    "id": "456",
    "name": "Introduction to Biology",
    "setupCompleted": true,
    "hasCourseDefaults": true,
    "adminToolPresetIds": ["a1b2c3d4-..."],
    "assessments": [
      {
        "id": "classicquiz_123",
        "title": "Midterm Exam",
        "contentType": "CLASSIC_QUIZ",
        "sebRequired": true,
        "enabled": true,
        "hasAccessCode": true,
        "verificationStatus": "verified"
      }
    ]
  }
}
```

***

### `GET /api/admin/course-catalog`

Browses active Canvas courses via the Canvas admin API in bounded, server-filtered pages. Each course entry is annotated with a `connected` boolean indicating whether it is already in the local connection index. Results come from Canvas directly and are not cached.

**Auth:** Verified root-account administrator. No action token required.

<ParamField query="search" type="string">
  Free-text course search forwarded to the Canvas API.
</ParamField>

<ParamField query="termId" type="string">
  Canvas enrollment term ID to filter by.
</ParamField>

<ParamField query="cursor" type="string">
  Pagination cursor from a previous response.
</ParamField>

<ParamField query="includeUnpublished" type="boolean">
  Pass `"true"` to include unpublished courses.
</ParamField>

<ParamField query="withEnrollments" type="boolean">
  Pass `"false"` to omit enrollment data. Defaults to `true`.
</ParamField>

***

### `GET /api/admin/terms`

Returns the list of active Canvas enrollment terms and the currently persisted operational term for the root account. The operational term defaults to the currently active term if none has been saved.

**Auth:** Verified root-account administrator. No action token required.

```json theme={null}
{
  "success": true,
  "terms": [
    { "id": "41", "name": "Spring 2024", "startAt": "2024-01-15", "endAt": "2024-05-15" },
    { "id": "42", "name": "Fall 2024", "startAt": "2024-08-26", "endAt": "2024-12-20" }
  ],
  "operationalTerm": { "id": "42", "name": "Fall 2024" }
}
```

***

### `PUT /api/admin/terms/operational` †

Persists the root account's shared operational term selection. The supplied `termId` must be present in the live Canvas term list; otherwise the request is rejected with `400`.

**Auth:** Verified root-account administrator. Action token required.

<ParamField body="termId" type="string" required>
  Numeric Canvas enrollment term ID.
</ParamField>

```json theme={null}
{
  "success": true,
  "operationalTerm": { "id": "42", "name": "Fall 2024" }
}
```

***

### `POST /api/admin/courses/connect` †

Validates, connects, and performs an initial Classic Quiz and New Quiz discovery for one or more Canvas courses. Each course is validated against the administrator's root-account boundary. Accepts a JSON body with either a `courseIds` array of numeric IDs or an `input` string of whitespace- or comma-separated IDs and Canvas course URLs. Maximum 50 courses per request.

**Auth:** Verified root-account administrator. Action token required.

<ParamField body="courseIds" type="string[]">
  Array of numeric Canvas course IDs.
</ParamField>

<ParamField body="input" type="string">
  Whitespace or comma-separated Canvas course IDs or full Canvas course URLs (e.g. `https://canvas.example.edu/courses/456`).
</ParamField>

```json theme={null}
{
  "success": true,
  "connectedCount": 2,
  "results": [
    { "courseId": "456", "success": true, "course": { "id": "456", "name": "Introduction to Biology" } },
    { "courseId": "999", "success": false, "error": "Course is outside this root account" }
  ]
}
```

***

### `POST /api/admin/courses/:courseId/refresh` †

Refreshes Classic Quiz and New Quiz discovery for one connected course using the administrator's account-level Canvas grant. Updates the course connection record and assessment counts.

**Auth:** Verified root-account administrator. Action token required. Course must be in the connection index.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

```json theme={null}
{
  "success": true,
  "assessmentCount": 5,
  "assessments": [
    { "id": "classicquiz_123", "title": "Midterm Exam", "sebRequired": true }
  ]
}
```

***

### `POST /api/admin/courses/:courseId/reset` †

<Warning>
  **This operation is destructive.** The reset disables every SEB-enabled Canvas assessment in the course (removing Canvas access codes), then deletes all local course setup in a single PostgreSQL transaction. Canvas OAuth authorization for the course users is deliberately **preserved**.

  The operation uses a two-phase approach with full compensation:

  1. **Preflight** — strict read-only Canvas discovery and access-code state capture. No changes are made yet.
  2. **Canvas mutations** — each access code is removed using the administrator grant.
  3. **Database transaction** — assessments, course policy, transient state, and per-course preset assignments are deleted atomically. A durable reset receipt is stored on the connection record.

  If a Canvas mutation fails, a later Canvas write fails, or the database transaction definitively fails, the service restores the exact pre-reset Canvas access codes and assessment records in reverse order. If the transaction commit response is ambiguous, the service reads the durable receipt before deciding whether to compensate. An unavailable receipt check, failed compensation, or lost operation lease is reported as `ADMIN_COURSE_RESET_VERIFY_REQUIRED` — an indeterminate result that requires manual refresh and verification.

  A `confirmation` field containing the exact numeric course ID must be present in the request body. Without it the request is rejected before any Canvas call is made.
</Warning>

**Auth:** Verified root-account administrator. Action token required. Course must be in the connection index.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField body="confirmation" type="string" required>
  Must equal the exact `:courseId` value. Used as a mandatory safety confirmation.
</ParamField>

**Error codes:**

| `error_code`                                         | HTTP | Meaning                                                                                             |
| ---------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------- |
| `ADMIN_COURSE_RESET_CONFIRMATION_REQUIRED`           | 400  | `confirmation` field is missing or does not match the course ID.                                    |
| `COURSE_RESET_IN_PROGRESS`                           | 409  | Another reset for this course is already running.                                                   |
| `ADMIN_COURSE_RESET_ASSESSMENT_IDENTITY_UNAVAILABLE` | 409  | Canvas did not return an identifier needed to remove an access code; records were not deleted.      |
| `ADMIN_COURSE_RESET_ROLLBACK_VERIFY_REQUIRED`        | 409  | Reset stopped; one or more Canvas access codes could not be restored. Manual verification required. |
| `ADMIN_COURSE_RESET_VERIFY_REQUIRED`                 | 409  | Transaction commit was ambiguous or a lock was lost. Refresh and verify before retrying.            |
| `ADMIN_COURSE_RESET_ASSESSMENT_BUSY`                 | 409  | An assessment operation was in flight; records were not deleted.                                    |
| `CANVAS_AUTHORIZATION_REQUIRED`                      | 401  | Canvas authorization expired mid-reset; records were not deleted.                                   |
| `CANVAS_PERMISSION_DENIED`                           | 403  | Canvas denied access to at least one assessment; records were not deleted.                          |

***

## Password and Access-Code Management

<Warning>
  All password reveal responses are **short-lived and no-store**. The server sets `Cache-Control: no-store` and related security headers. Each response carries `expiresInSeconds: 30` as a client hint. The Canvas access code is included in the assessment-level reveal. Managed server-default exit passwords are **never** returned to the browser.
</Warning>

### `POST /api/admin/courses/:courseId/passwords/reveal` †

Reveals the course-level start and exit passwords. The effective exit password resolution order is: course-level override → managed server default → none.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

```json theme={null}
{
  "success": true,
  "expiresInSeconds": 30,
  "passwords": {
    "start": { "value": null, "source": "none" },
    "exit": { "value": "hunter2", "source": "course" }
  }
}
```

***

### `POST /api/admin/courses/:courseId/quit-password/rotate` †

Generates a new cryptographically random exit password for the course, saves it as the course-level override, and returns the new value in the no-store response. Students currently in an active SEB session will use the new password when SEB prompts them to quit.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

```json theme={null}
{
  "success": true,
  "expiresInSeconds": 30,
  "passwords": {
    "exit": { "value": "xK9mP2rT", "source": "course" }
  }
}
```

***

### `POST /api/admin/courses/:courseId/assessments/:assessmentId/passwords/reveal` †

Reveals start password, exit password, and Canvas access code for a single assessment. Each value includes a `source` indicating whether it is an assessment-level override, a course default, a managed server default (value withheld), or unset.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField path="assessmentId" type="string" required>
  Canonical content ID (`classicquiz_{id}` or `newquiz:{courseId}:{assignmentId}`).
</ParamField>

```json theme={null}
{
  "success": true,
  "expiresInSeconds": 30,
  "passwords": {
    "start": { "value": "s3cr3t", "source": "assessment" },
    "exit": { "value": null, "source": "managed" },
    "accessCode": { "value": "ABC123", "source": "canvas" }
  }
}
```

***

### `POST /api/admin/courses/:courseId/assessments/:assessmentId/quit-password/reset` †

Resets the assessment's exit password to its effective default (course override or managed default), removing any assessment-level override. Does not rotate the underlying password — it removes the override so the assessment inherits the course or managed value.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField path="assessmentId" type="string" required>
  Canonical content ID.
</ParamField>

***

### `POST /api/admin/courses/:courseId/assessments/:assessmentId/reset-defaults` †

Returns one assessment to course defaults: clears URL policy, password overrides, and tool-ID selections so the assessment inherits the full course catalog. Does not change the Canvas access code or `sebRequired` state.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField path="assessmentId" type="string" required>
  Canonical content ID.
</ParamField>

***

### `POST /api/admin/courses/:courseId/assessments/:assessmentId/regenerate-code` †

Generates a new Canvas access code and applies it to the assessment. Dispatches to the Classic Quiz or New Quiz Canvas API based on the content ID format. Returns the updated SEB setting view on success. Returns `409` if SEB is not currently enabled for the assessment.

**Auth:** Verified root-account administrator. Action token required. Uses the administrator's account-level Canvas grant.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField path="assessmentId" type="string" required>
  Canonical content ID.
</ParamField>

***

### `PUT /api/admin/courses/:courseId/assessments/:assessmentId/seb` †

Enables or disables SEB enforcement on a single assessment. Acquires both the course-level write lock and the assessment operation lock; ordinary instructor mutations cannot overlap this call. After the Canvas mutation and local state change, the connection record's assessment counts are updated atomically.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField path="assessmentId" type="string" required>
  Canonical content ID.
</ParamField>

<ParamField body="required" type="boolean" required>
  `true` to enable SEB and set the Canvas access code; `false` to disable and remove it.
</ParamField>

***

## Tool Presets

School tool presets are reusable exam-tool definitions stored at the root-account level in `admin_tool_presets`. Assigning a preset to a course synchronizes it into the course catalog as a school-managed entry; instructors may enable or disable it but cannot change its launch URL or resource rules. Each account may have at most 32 presets.

### `GET /api/admin/tool-presets`

Returns all tool presets for the administrator's root account, including assignment counts and rollout status.

**Auth:** Verified root-account administrator. No action token required.

```json theme={null}
{
  "success": true,
  "presets": [
    {
      "id": "a1b2c3d4-...",
      "name": "Desmos Calculator",
      "description": "Graphing calculator for STEM assessments",
      "tool": { "url": "https://www.desmos.com/scientific", "label": "Desmos Calculator" },
      "assignedCourseIds": ["456", "789"],
      "assignedCourseCount": 2,
      "pendingAssignmentCount": 0,
      "failedAssignmentCount": 0
    }
  ]
}
```

***

### `POST /api/admin/tool-presets` †

Creates a new school tool preset. The tool definition requires an exact HTTPS launch URL. Resource access rules must be explicit HTTPS URLs or confirmed domains — regex patterns, wildcards, and broad domain rules without `broadDomainConfirmed: true` are rejected. Preset names must be 1–80 characters; descriptions must not exceed 240 characters.

**Auth:** Verified root-account administrator. Action token required.

<ParamField body="name" type="string" required>
  Preset display name, 1–80 characters.
</ParamField>

<ParamField body="description" type="string">
  Optional description, up to 240 characters.
</ParamField>

<ParamField body="tool" type="ExternalToolConfig" required>
  Tool definition. Must include an exact HTTPS `url`. Resource rules go in `allowedRules`; each rule with `match: "domain"` must have `broadDomainConfirmed: true`.
</ParamField>

***

### `PUT /api/admin/tool-presets/:presetId` †

Updates an existing preset's name, description, and tool definition. After saving, all active course assignments for the preset are marked `"pending"` so the next reconciliation run propagates the change to every assigned course.

**Auth:** Verified root-account administrator. Action token required. The preset must belong to the administrator's root account.

<ParamField path="presetId" type="string" required>
  UUID of the preset to update.
</ParamField>

***

### `DELETE /api/admin/tool-presets/:presetId` †

Deletes a preset. The preset must have no active (`desiredAssigned: true`) course assignments; otherwise the request returns `409`. Completed (`applied`) assignment records for unassigned courses are cleaned up automatically before deletion.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="presetId" type="string" required>
  UUID of the preset to delete.
</ParamField>

***

### `PUT /api/admin/tool-presets/:presetId/courses/:courseId` †

Assigns or unassigns a preset for a single Canvas course. Sets the desired assignment state, then immediately attempts to apply it by running one reconciliation pass. If the synchronization fails, the assignment is saved with `status: "failed"` for later retry — the response body indicates failure with `error_code: "ADMIN_TOOL_PRESET_ROLLOUT_FAILED"` but the endpoint returns `502` rather than silently succeeding.

**Auth:** Verified root-account administrator. Action token required. The course must be in the connection index and within the administrator's root account.

<ParamField path="presetId" type="string" required>
  UUID of the preset.
</ParamField>

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID.
</ParamField>

<ParamField body="assigned" type="boolean" required>
  `true` to assign; `false` to unassign.
</ParamField>

***

### `PUT /api/admin/tool-presets/:presetId/assignments` †

Queues a selected-course or all-course rollout for a preset. Saves desired assignment state for all specified courses, then synchronizes a bounded batch (`ADMIN_RECONCILE_BATCH_SIZE = 12`) immediately. Maximum 2,000 connected courses per rollout request.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="presetId" type="string" required>
  UUID of the preset.
</ParamField>

<ParamField body="assigned" type="boolean" required>
  `true` to assign; `false` to unassign.
</ParamField>

<ParamField body="all" type="boolean" required>
  When `true`, the rollout targets all connected courses for the root account. `courseIds` is ignored when `all` is `true`.
</ParamField>

<ParamField body="courseIds" type="string[]">
  Explicit list of numeric Canvas course IDs. Required when `all` is `false`.
</ParamField>

```json theme={null}
{
  "success": false,
  "queuedCount": 18,
  "preset": { "id": "a1b2c3d4-...", "pendingAssignmentCount": 6 },
  "rollout": {
    "processed": 12,
    "applied": 11,
    "failed": 1,
    "pending": 5
  }
}
```

***

### `POST /api/admin/tool-presets/:presetId/assignments/reconcile` †

Processes a bounded batch of pending (and optionally failed) assignments for a preset. Each assignment is applied under both a preset-specific operation lock and the course write lock, so it cannot race against a concurrent course reset or tool copy. This endpoint is used for incremental rollout processing and retry.

**Auth:** Verified root-account administrator. Action token required.

<ParamField path="presetId" type="string" required>
  UUID of the preset.
</ParamField>

<ParamField body="retryFailed" type="boolean">
  When `true`, assignments in `"failed"` state are retried alongside `"pending"` ones. Defaults to `false`.
</ParamField>

```json theme={null}
{
  "success": true,
  "preset": { "id": "a1b2c3d4-...", "pendingAssignmentCount": 0, "failedAssignmentCount": 0 },
  "rollout": {
    "processed": 6,
    "applied": 6,
    "failed": 0,
    "pending": 0
  }
}
```

***

## Assessment View Reference

Admin routes that return assessment lists use the `adminAssessmentView` shape:

<ResponseField name="id" type="string">
  Canonical content ID (`classicquiz_{id}` or `newquiz:{courseId}:{assignmentId}`).
</ResponseField>

<ResponseField name="courseId" type="string">
  Numeric Canvas course ID.
</ResponseField>

<ResponseField name="contentType" type="string">
  `"CLASSIC_QUIZ"` or `"NEW_QUIZ"`.
</ResponseField>

<ResponseField name="title" type="string">
  Assessment title from the last Canvas discovery run.
</ResponseField>

<ResponseField name="published" type="boolean">
  Whether the assessment is published in Canvas.
</ResponseField>

<ResponseField name="sebRequired" type="boolean">
  Whether SEB enforcement is currently required.
</ResponseField>

<ResponseField name="enabled" type="boolean">
  Whether local SEB state is enabled (may differ from `sebRequired` during transitions).
</ResponseField>

<ResponseField name="hasAccessCode" type="boolean">
  Whether a Canvas access code is currently set.
</ResponseField>

<ResponseField name="hasStartPassword" type="boolean">
  Whether a start password is configured.
</ResponseField>

<ResponseField name="hasQuitPassword" type="boolean">
  Whether an exit password is configured.
</ResponseField>

<ResponseField name="usesCourseDefaults" type="boolean">
  Whether the assessment inherits URL policy and tool catalog from course defaults.
</ResponseField>

<ResponseField name="verificationStatus" type="string">
  Canvas discovery status: `"verified"`, `"missing"`, `"stale"`, or `"unverified"` for legacy records.
</ResponseField>

<ResponseField name="updatedAt" type="string | null">
  ISO 8601 timestamp of the last local write.
</ResponseField>

***

## Common Error Codes

<Accordion title="Course and connection errors">
  | `error_code`                 | HTTP | Meaning                                                                                                      |
  | ---------------------------- | ---- | ------------------------------------------------------------------------------------------------------------ |
  | `ADMIN_COURSE_NOT_CONNECTED` | 404  | Course ID is not in the root account's connection index.                                                     |
  | `INVALID_ADMIN_COURSE_IDS`   | 400  | No valid numeric course IDs were supplied, or more than 50 were provided.                                    |
  | `ASSESSMENT_NOT_FOUND`       | 404  | The assessment ID is not canonical, or the record does not belong to the specified course.                   |
  | `SEB_NOT_ENABLED`            | 409  | A code regeneration was attempted on an assessment that has SEB disabled.                                    |
  | `ASSESSMENT_ID_UNAVAILABLE`  | 409  | Canvas did not return a required assessment identifier; the content ID cannot be used for a Canvas mutation. |
</Accordion>

<Accordion title="Tool preset errors">
  | `error_code`                       | HTTP | Meaning                                                                               |
  | ---------------------------------- | ---- | ------------------------------------------------------------------------------------- |
  | `ADMIN_TOOL_PRESET_NOT_FOUND`      | 404  | Preset UUID is invalid or does not belong to the administrator's root account.        |
  | `ADMIN_TOOL_PRESET_LIMIT`          | 409  | The account already has 32 presets, the maximum allowed.                              |
  | `ADMIN_TOOL_PRESET_ASSIGNED`       | 409  | The preset still has active course assignments; unassign all courses before deleting. |
  | `ADMIN_TOOL_PRESET_ROLLOUT_FAILED` | 502  | Single-course assignment was saved but synchronization failed; retry via reconcile.   |
  | `ADMIN_TOOL_PRESET_ROLLOUT_LIMIT`  | 409  | Bulk rollout exceeds 2,000 connected courses.                                         |
  | `INVALID_ADMIN_TOOL_PRESET`        | 400  | Preset name, description, or tool definition did not pass validation.                 |
</Accordion>

<Accordion title="Canvas authorization errors">
  When Canvas rejects the stored OAuth token during a mutation, the server returns a descriptive error. For the course reset specifically, the response is `401` with `CANVAS_AUTHORIZATION_REQUIRED` and a message explaining that local records were not deleted.
</Accordion>
