> ## 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.

# Instructor Assessment Management Routes — Safe Online Exam

> Reference for /api/quizzes: discovery, SEB enable/disable, course defaults, exam-tool copy, and session-bound password reveal for verified instructors.

Safe Online Exam exposes all instructor-facing operations under the `/api/quizzes` prefix. This page covers every route an instructor's browser calls when managing SEB policy for Classic Quizzes and New Quizzes in their Canvas course — from reading the cached assessment list through enabling SEB and rotating access codes to the short-lived password reveal flow.

<Note>
  Every route in this group requires a **verified LTI instructor principal** stored in the Express session. That principal is created only after a successful, fully-validated LTI 1.3 launch from Canvas; query parameters and request bodies cannot substitute for it. Mutation routes (marked with a `†` below) additionally require the request to pass the same-origin Fetch Metadata integrity check enforced by the application's HTTP middleware.
</Note>

***

## Discovery Routes

These read-only endpoints return assessment data cached from the most recent Canvas discovery run. They do not make live Canvas API calls.

### `GET /api/quizzes`

Returns all Classic Quizzes and New Quizzes cached for the instructor's current course. The response is a bare JSON array — there is no `success` envelope.

**Auth:** Verified instructor principal. No action token required.

```json theme={null}
[
  {
    "id": "classicquiz_123",
    "title": "Midterm Exam",
    "courseId": "456",
    "contentType": "CLASSIC_QUIZ",
    "published": true
  }
]
```

***

### `GET /api/quizzes/:quizId`

Returns one cached assessment. The `:quizId` parameter accepts either a canonical content ID (`classicquiz_{id}` or `newquiz:{courseId}:{assignmentId}`) or a bare numeric Canvas quiz ID for Classic Quizzes.

**Auth:** Verified instructor principal. The server re-checks that the requested assessment belongs to the session course.

<ParamField path="quizId" type="string" required>
  Canonical content ID or bare Canvas quiz ID for a Classic Quiz.
</ParamField>

***

### `GET /api/quizzes/seb-settings`

Returns a map of SEB settings keyed by quiz ID for every assessment in the current course. Each value is a redacted settings view — access codes and passwords are not included in this response.

**Auth:** Verified instructor principal.

```json theme={null}
{
  "classicquiz_123": {
    "sebRequired": true,
    "enabled": true,
    "hasAccessCode": true,
    "usesCourseDefaults": false
  }
}
```

***

### `POST /api/quizzes/course/:courseId/refresh` †

Triggers a fresh Classic Quiz and New Quiz discovery from Canvas for the given course. Uses the instructor's stored Canvas OAuth grant to call the Canvas REST and New Quiz APIs. On success, returns the updated quiz list with counts.

**Auth:** Verified instructor principal scoped to `:courseId`. Requires Canvas OAuth authorization.

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

If Canvas authorization has expired, the response includes `requiresAuth: true` and an `authUrl` for the reauthorization flow rather than a hard error.

```json theme={null}
{
  "success": true,
  "message": "Quiz data refreshed successfully",
  "quizCount": 4,
  "quizzes": [
    { "id": "classicquiz_123", "title": "Midterm Exam", "canvasQuizId": "123" }
  ]
}
```

***

## Course Defaults Routes

### `GET /api/quizzes/course/:courseId/defaults`

Returns the course-level SEB defaults: URL policy, exit and start passwords (redacted), and the exam-tool catalog. Passwords in this response are never revealed — use the password reveal endpoint to read them.

**Auth:** Verified instructor principal scoped to `:courseId`.

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

```json theme={null}
{
  "success": true,
  "defaults": {
    "courseId": "456",
    "urlRules": [],
    "externalTools": [],
    "setupCompleted": true,
    "hasQuitPassword": true,
    "hasStartPassword": false
  }
}
```

***

### `PUT /api/quizzes/course/:courseId/defaults` †

Saves updated course defaults. Accepts URL policy rules, exit and start password values, and the full exam-tool catalog. All URL rules must be exact HTTPS URLs or concrete domains — regex and wildcard patterns are rejected. When `externalTools` is present in the body, the server replaces the course catalog with the supplied list after normalization.

**Auth:** Verified instructor principal scoped to `:courseId`. Mutation guard required.

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

**Request body fields:**

<ParamField body="urlRules" type="SebUrlRule[]">
  Array of URL policy rules. Each rule must be an exact HTTPS URL or a concrete domain without wildcards.
</ParamField>

<ParamField body="externalTools" type="ExternalToolConfig[]">
  Replacement exam-tool catalog for the course. Each tool requires an exact HTTPS launch URL and explicit resource access rules.
</ParamField>

<ParamField body="quitPassword" type="string | null">
  New exit password, or `null` to remove the course-level override.
</ParamField>

<ParamField body="startPassword" type="string | null">
  New start password, or `null` to remove the course-level override.
</ParamField>

<ParamField body="setupCompleted" type="boolean">
  When `false`, marks course setup as incomplete.
</ParamField>

***

## SEB Management Routes

These routes commit or remove Canvas access codes and update SEB state. Each mutation acquires a short-lived PostgreSQL operation lock on the assessment record; overlapping requests for the same assessment are rejected with `409`.

### `PUT /api/quizzes/:quizId/seb` †

Enables or disables SEB for the session-course assessment identified by `:quizId`. When enabling (`required: true`), the server creates a Canvas access code and persists SEB state only after the Canvas mutation succeeds. When disabling (`required: false`), the access code is removed from Canvas before local state is cleared.

**Auth:** Verified instructor principal. The server re-validates that `:quizId` belongs to the session course.

<ParamField path="quizId" type="string" required>
  Canonical content ID or bare Classic Quiz ID.
</ParamField>

<ParamField body="required" type="boolean" required>
  `true` to enable SEB; `false` to disable.
</ParamField>

***

### `POST /api/quizzes/seb-config-structured` †

A structured alternative to the simple enable/disable toggle. Accepts a `StructuredSebConfigRequest` body and saves URL policy, password overrides, quiz-only tool definitions, and tool-ID selections for a single assessment without changing the `sebRequired` state. Exam-tool definitions (`externalTools`) are managed at the course level; submitting that field returns `400`.

**Auth:** Verified instructor principal. Optional `userId` query parameter is validated against the session user.

<ParamField query="userId" type="string">
  Canvas user ID. When present, must match the session principal's user ID.
</ParamField>

<ParamField body="contentId" type="string">
  Canonical New Quiz content ID (`newquiz:{courseId}:{assignmentId}`). Use `quizId` for Classic Quizzes.
</ParamField>

<ParamField body="quizId" type="string">
  Canonical Classic Quiz content ID or bare Canvas quiz ID.
</ParamField>

<ParamField body="urlRules" type="SebUrlRule[]">
  Per-assessment URL policy rules. Overrides course defaults when `usesCourseDefaults` is `false`.
</ParamField>

<ParamField body="quitPassword" type="string | null">
  Exit password override. Only applied when `quitPasswordOverride` is `true`.
</ParamField>

<ParamField body="startPassword" type="string | null">
  Start password override. Only applied when `startPasswordOverride` is `true`.
</ParamField>

<ParamField body="usesCourseDefaults" type="boolean">
  When `true`, the assessment inherits URL policy and tool catalog from course defaults.
</ParamField>

<ParamField body="quitPasswordOverride" type="boolean">
  When `true`, uses the assessment-level exit password instead of the course default.
</ParamField>

<ParamField body="startPasswordOverride" type="boolean">
  When `true`, uses the assessment-level start password instead of the course default.
</ParamField>

<ParamField body="externalToolIds" type="string[] | null">
  Explicit allowlist of course-tool IDs to enable for this assessment. `null` means inherit the full course catalog.
</ParamField>

<ParamField body="quizOnlyExternalTools" type="ExternalToolConfig[]">
  Assessment-scoped tool definitions that are merged with the course catalog. These are never stored as course tools.
</ParamField>

***

### `POST /api/quizzes/:courseId/:quizId/seb/enable` †

Enables SEB for a specific assessment and sets the Canvas access code. The server dispatches to the Classic Quiz or New Quiz Canvas API based on the content ID format. Requires an effective exit password (assessment override, course default, or configured managed default) or the request is rejected.

**Auth:** Verified instructor principal scoped to `:courseId` and `:quizId`.

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

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

```json theme={null}
{
  "success": true,
  "message": "Safe Online Exam enabled.",
  "setting": {
    "sebRequired": true,
    "enabled": true,
    "hasAccessCode": true,
    "configValid": true
  }
}
```

***

### `POST /api/quizzes/:courseId/:quizId/seb/disable` †

Disables SEB for a specific assessment and removes the Canvas access code.

**Auth:** Verified instructor principal scoped to `:courseId` and `:quizId`.

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

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

```json theme={null}
{
  "success": true,
  "message": "Safe Online Exam disabled.",
  "setting": {
    "sebRequired": false,
    "enabled": false,
    "hasAccessCode": false
  }
}
```

***

### `POST /api/quizzes/:courseId/:quizId/seb/reset-defaults` †

Returns one assessment to course defaults. Clears any assessment-level URL policy, password overrides, and tool-ID selections so the assessment inherits the course catalog.

**Auth:** Verified instructor principal scoped to `:courseId` and `:quizId`.

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

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

***

### `POST /api/quizzes/:courseId/:quizId/seb/regenerate-code` †

Generates a new Canvas access code and applies it to the Canvas assessment. Students already on the quiz page must reload — the previous access code is immediately invalid. Returns only a success message; the new code is not included in the response.

**Auth:** Verified instructor principal scoped to `:courseId` and `:quizId`. Requires Canvas OAuth authorization.

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

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

```json theme={null}
{
  "success": true,
  "message": "Safe Online Exam access code regenerated. Students should reopen the quiz from Canvas."
}
```

***

### `GET /api/quizzes/:courseId/:quizId/seb/config`

Redirects (HTTP 302) to the current SEB configuration flow at `/seb/launch/:quizId`. This is an instructor convenience shortcut; it does not require Canvas OAuth.

**Auth:** Verified instructor principal. No mutation guard.

<ParamField path="courseId" type="string" required>
  Numeric Canvas course ID (used for route matching; not included in the redirect target).
</ParamField>

<ParamField path="quizId" type="string" required>
  Canonical content ID. Used as the redirect target.
</ParamField>

***

### `GET /api/quizzes/:courseId/:quizId/seb/status`

Returns a secret-free status view for one assessment: whether SEB is enabled and required, whether an access code exists, and whether the current configuration is valid (has a required exit password). Access codes and passwords are not included.

**Auth:** Verified instructor principal. No mutation guard.

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

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

```json theme={null}
{
  "authenticated": true,
  "sebEnabled": true,
  "hasAccessCode": true,
  "configValid": true,
  "canvasDomain": "canvas.example.edu",
  "ssoDomains": [],
  "educationalToolDomains": [],
  "customDomains": [],
  "externalTools": []
}
```

***

## Exam Tool Routes

### `GET /api/quizzes/course/:courseId/exam-tools/:toolId/copy-targets`

Returns a list of other Canvas courses where the instructor is a teacher and where the specified exam tool can be copied. The picker list is a convenience view; the server re-authorizes the live Canvas list before any copy operation.

**Auth:** Verified instructor principal. Requires Canvas OAuth. School-managed tools (`managedByAdmin: true`) cannot be copied; the route returns `409` for those.

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

<ParamField path="toolId" type="string" required>
  Local tool ID from the course exam-tool catalog.
</ParamField>

```json theme={null}
{
  "success": true,
  "courses": [
    { "courseId": "789", "name": "Fall Chemistry", "courseCode": "CHEM101" }
  ]
}
```

***

### `POST /api/quizzes/course/:courseId/exam-tools/:toolId/copy` †

Copies one instructor-owned exam tool into one or more target courses. Before writing, the server:

1. Snapshots the reset generation for every target course.
2. Re-reads the live Canvas teacher-course list for the user.
3. Rejects any target not present in the live Canvas response.
4. Rejects any target whose reset generation has advanced since the snapshot.

The copy appends a local tool definition without replacing the target catalog and preserves an existing equivalent definition on retry.

**Auth:** Verified instructor principal scoped to `:courseId`. Mutation guard required. Maximum 100 targets per request.

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

<ParamField path="toolId" type="string" required>
  Local tool ID from the source course catalog.
</ParamField>

<ParamField body="courseIds" type="string[]" required>
  Array of numeric Canvas course IDs to receive the tool. The source course ID must not appear in this list.
</ParamField>

```json theme={null}
{
  "success": true,
  "copied": [{ "courseId": "789", "name": "Fall Chemistry", "courseCode": "CHEM101", "status": "copied" }],
  "alreadyPresent": [],
  "failed": []
}
```

***

## Password Reveal Routes

<Warning>
  Password reveal responses are **short-lived and no-store**. The server sets `Cache-Control: no-store` and related security response headers on every reveal response. The client receives a 30-second expiry hint in `expiresInSeconds`. Managed server-default exit passwords are deliberately **never** returned to the browser — only course-level and assessment-level overrides are revealed.
</Warning>

### `POST /api/quizzes/course/:courseId/passwords/reveal` †

Reveals the course-level start and exit passwords for the given course. If the exit password source is `"managed"` (a server-configured default), the value is returned as `null` even though an effective password exists.

**Auth:** Verified instructor principal scoped to `:courseId`. Mutation guard 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" }
  }
}
```

The `source` field indicates where the password originates: `"assessment"` (assessment-level override), `"course"` (course-level override), `"managed"` (server-configured default, value withheld), or `"none"` (no password set).

***

### `POST /api/quizzes/:courseId/:quizId/passwords/reveal` †

Reveals the start and exit passwords for a single assessment. The `source` field distinguishes whether each password is inherited from the course or set as an assessment-level override.

**Auth:** Verified instructor principal scoped to `:courseId` and `:quizId`. Mutation guard required.

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

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

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

***

## Content ID Reference

Safe Online Exam uses canonical content IDs as stable assessment identifiers in URL path segments and request/response bodies.

<Tabs>
  <Tab title="Classic Quiz">
    ```text theme={null}
    classicquiz_{canvasQuizId}
    ```

    Example: `classicquiz_123`

    Bare numeric IDs (e.g. `123`) are accepted as path parameters for backward compatibility and are normalized internally to the canonical form.
  </Tab>

  <Tab title="New Quiz">
    ```text theme={null}
    newquiz:{courseId}:{assignmentId}
    ```

    Example: `newquiz:456:789`

    Both segments must be alphanumeric identifiers of 1–128 characters. The `courseId` embedded in the content ID is validated against the session principal's course.
  </Tab>
</Tabs>

***

## Common Error Codes

<Accordion title="Canvas authorization errors">
  When Canvas rejects the stored OAuth token, mutation routes return `success: false` with `requiresAuth: true` and an `authUrl` that opens the reauthorization flow in a popup. The LTI session page remains open.

  ```json theme={null}
  {
    "success": false,
    "requiresAuth": true,
    "message": "Canvas rejected the saved authorization. Reauthorize Canvas access to continue.",
    "authUrl": "/api/oauth2reauthorize?..."
  }
  ```
</Accordion>

<Accordion title="Policy validation errors">
  URL rules, domain lists, and exam-tool definitions are validated before any database write. Common error codes:

  | `error_code`                        | Meaning                                                                                            |
  | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
  | `INVALID_SEB_URL_POLICY`            | A URL rule uses a regex, wildcard, or unsafe pattern.                                              |
  | `INVALID_SEB_DOMAIN_POLICY`         | A domain entry uses a wildcard or is not a concrete hostname.                                      |
  | `INVALID_SEB_TOOL_POLICY`           | A tool definition lacks an exact HTTPS launch URL or uses unsafe resource rules.                   |
  | `INVALID_SEB_TOOL_SELECTION`        | `externalToolIds` is not a list of course tool IDs.                                                |
  | `QUIZ_TOOL_DEFINITIONS_NOT_ALLOWED` | `externalTools` was submitted to the structured endpoint; manage tools in Course settings instead. |
</Accordion>

<Accordion title="Operation lock conflicts">
  When a Canvas mutation is already in progress for an assessment, subsequent requests return `409` with one of:

  | `error_code`                | Meaning                                                      |
  | --------------------------- | ------------------------------------------------------------ |
  | `COURSE_RESET_IN_PROGRESS`  | An administrator reset is running; retry after it completes. |
  | `COURSE_UPDATE_IN_PROGRESS` | Another course-level write is in flight.                     |
</Accordion>
