> ## Documentation Index
> Fetch the complete documentation index at: https://theteam-13fc7f49.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Chapters

Manage chapters inside a training: list, create, reorder, show, update, and permanently delete. Chapters carry lessons and **any number of chapter quizzes** (since 19/10/2026). Quizzes are created and edited through the [Quizzes & Exams endpoints](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-endpoints.md) with a `chapter_id`.

All endpoints are authenticated — they run behind `auth:sanctum`, the "user is not blocked" check, and the per-company policy middleware. Always send your Sanctum token as a `Bearer` header.

## Table of contents

* **Base URL & Headers**
* **Chapter quizzes in the payload**
* **List chapters**
* **Reorder chapters**
* **Create a chapter**
* **Retrieve a chapter**
* **Update a chapter**
* **Permanently delete a chapter**
* **Postman setup**

***

## Base URL & Headers

```text theme={null}
Base URL: {{base_url}}/api
```

```text theme={null}
Accept: application/json
Authorization: Bearer {{token}}
Content-Type: application/json
```

Dates are returned in `Y-m-d H:i:s` format. Boolean query params accept `true`, `false`, `1`, `0`.

> Chapter `position` is 1-based and unique per training. A new chapter is **not** auto-positioned — you must send the `position` you want.

***

## Chapter quizzes in the payload

Since 19/10/2026 a chapter can hold **any number of quizzes** (before, adding a second quiz turned the first one into an exam). Every chapter object returned by `GET /chapters` and `GET /chapter/{id}` therefore carries two keys:

| Key | Type | Description |
| :- | :- | :- |
| `quizzes` | array of quiz objects | **Every** quiz of the chapter, in creation order (`id` ascending), each with its `quiz_questions` → `quiz_question_options` → `quiz_question_option_items` tree. `[]` when the chapter has none. Present whether or not the chapter has lessons. |
| `quiz` | quiz object or `null` | ⚠️ **Deprecated.** The first element of `quizzes` (same object), or `null`. Kept so older app builds that read a single chapter quiz keep working. Read `quizzes` in new code. |

Who sees what:

* **Students** only get the **published** chapter quizzes (`is_published = 1`), in both keys.
* **Staff** (instructor, content manager, admin, super admin) also get the **drafts**, so editors can list them. Check each quiz's `is_published`.

Each quiz is a full `Quiz` row: `training_id` is `null` for a chapter quiz (it is found through `chapter_id`), it carries its `slug`, its `instructor` and the computed rating / sales fields. The same two keys appear in the chapters of [`GET /training/{id}`](https://theteam-13fc7f49.mintlify.site/api-reference/trainings#retrieve-a-training), the login payload ([`GET /auth/user`](https://theteam-13fc7f49.mintlify.site/api-reference/auth)) and the [chapter exports](https://theteam-13fc7f49.mintlify.site/api-reference/chapters-and-lessons-export-import.md).

> ⚠️ Like `GET /training/{id}`, these endpoints return each quiz's options with their `is_correct` flags and `explanation` texts. Use them for the course player of an owned training and for the editors, never for the marketplace.

***

## List chapters

`GET /chapters`

Returns a paginated list of chapters for a given training. Each chapter always includes its `quizzes` (plus the deprecated `quiz`, see [Chapter quizzes in the payload](#chapter-quizzes-in-the-payload)), each with its question tree (questions → options → option items). Lessons are included only when you ask for them.

### Query parameters

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `training_id` | integer | Yes | Must exist in the `trainings` table. |
| `include_lessons` | boolean | No | When `true`, eager-loads the `lessons` relation on each chapter. |

### Example request

```text theme={null}
GET {{base_url}}/api/chapters?training_id=12&include_lessons=true
Accept: application/json
Authorization: Bearer {{token}}
```

### Example response — `200 OK`

Called by an instructor: chapter 40 has a published quiz and a draft, chapter 41 has no lessons but still returns its quiz. A student would not see quiz `78` (draft).

```text theme={null}
{
  "current_page": 1,
  "data": [
    {
      "id": 40,
      "training_id": 12,
      "label": "Chapter 1 — Fundamentals",
      "position": 1,
      "lessons_count": 3,
      "created_at": "2026-04-01 09:22:10",
      "updated_at": "2026-04-10 14:03:55",
      "archived_at": null,
      "deleted_at": null,
      "quizzes": [
        {
          "id": 77,
          "company_id": 1,
          "training_id": null,
          "chapter_id": 40,
          "instructor_id": 5,
          "image_id": null,
          "label": "Chapter 1 Quiz",
          "slug": "chapter-1-quiz",
          "description": "Check the fundamentals.",
          "max_attempts": 3,
          "passing_percentage": 70,
          "duration": 15,
          "break_interval": null,
          "break_duration": null,
          "is_published": 1,
          "price": null,
          "discounted_price": null,
          "revenue_percentage": 0,
          "created_at": "2026-04-02 10:00:00",
          "updated_at": "2026-04-02 10:00:00",
          "average_rating": 0,
          "total_ratings": 0,
          "total_sales": 0,
          "final_price": 0,
          "total_active_sales": 0,
          "instructor": { "id": 5, "first_name": "Ali", "last_name": "Ben", "...": "..." },
          "quiz_questions": [
            {
              "id": 300,
              "quiz_id": 77,
              "quiz_question_type_id": 2,
              "image_id": null,
              "label": "What does the project scope define?",
              "tip": null,
              "explanation": "Scope = the work required to deliver the result.",
              "quiz_question_options": [
                { "id": 900, "quiz_question_id": 300, "label": "The work to be done", "is_correct": true, "quiz_question_option_items": [] },
                { "id": 901, "quiz_question_id": 300, "label": "The project budget", "is_correct": false, "quiz_question_option_items": [] }
              ]
            }
          ]
        },
        {
          "id": 78,
          "training_id": null,
          "chapter_id": 40,
          "label": "Chapter 1 Practice Quiz",
          "slug": "chapter-1-practice-quiz",
          "is_published": 0,
          "...": "same fields as above",
          "quiz_questions": []
        }
      ],
      "lessons": [
        {
          "id": 101,
          "chapter_id": 40,
          "label": "Lesson 1",
          "type": "document",
          "position": 1,
          "content": "<p>...</p>"
        }
      ],
      "quiz": { "id": 77, "chapter_id": 40, "label": "Chapter 1 Quiz", "...": "same object as quizzes[0] (deprecated)" }
    },
    {
      "id": 41,
      "training_id": 12,
      "label": "Chapter 2 — Checkpoint",
      "position": 2,
      "lessons_count": 0,
      "created_at": "2026-04-03 08:10:00",
      "updated_at": "2026-04-03 08:10:00",
      "archived_at": null,
      "deleted_at": null,
      "quizzes": [
        { "id": 79, "training_id": null, "chapter_id": 41, "label": "Checkpoint Quiz", "slug": "checkpoint-quiz", "is_published": 1, "...": "...", "quiz_questions": [ "..." ] }
      ],
      "lessons": [],
      "quiz": { "id": 79, "chapter_id": 41, "label": "Checkpoint Quiz", "...": "same object as quizzes[0] (deprecated)" }
    }
  ],
  "per_page": 15,
  "total": 4,
  "last_page": 1
}
```

A chapter without any quiz returns `"quizzes": []` and `"quiz": null`.

***

## Reorder chapters

`POST /chapters/reorder`

Bulk-updates the `position` of multiple chapters in a single call. The request body is a **bare array** — not an object.

### Body

An array of `{ id, position }` pairs.

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `id` | integer | Yes | Must exist in `chapters`. |
| `position` | integer | Yes | Minimum `1`. |

### Example request

```text theme={null}
POST {{base_url}}/api/chapters/reorder
Accept: application/json
Authorization: Bearer {{token}}
Content-Type: application/json
```

```text theme={null}
[
  { "id": 40, "position": 2 },
  { "id": 41, "position": 1 },
  { "id": 42, "position": 3 }
]
```

### Response — `200 OK`

```text theme={null}
{ "message": "Chapters reordered successfully." }
```

***

## Create a chapter

`POST /chapter`

### Body parameters

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `training_id` | integer | Yes | Must exist in `trainings`. |
| `label` | string | Yes | 1–255 characters. |
| `position` | integer | Yes | Minimum `1`. Unique per training. |

### Example request

```text theme={null}
POST {{base_url}}/api/chapter
Accept: application/json
Authorization: Bearer {{token}}
Content-Type: application/json
```

```text theme={null}
{
  "training_id": 12,
  "label": "Chapter 1 — Fundamentals",
  "position": 1
}
```

### Response — `200 OK`

```text theme={null}
{
  "chapter_id": 40,
  "message": "Chapter created successfully."
}
```

***

## Retrieve a chapter

`GET /chapter/{id}`

Returns a single chapter with **all its quizzes** (`quizzes`, creation order) and their question trees (questions → options → option items), plus the deprecated `quiz` (= the first one). Students only get the published quizzes; staff also get the drafts — see [Chapter quizzes in the payload](#chapter-quizzes-in-the-payload). Lessons are **not** included in this endpoint — use `GET /lessons?chapter_id={id}` to list them.

> Before 19/10/2026 this endpoint returned the chapter's single `quiz` whatever its publish state, so a student could receive a draft quiz here while `GET /training/{id}` (published only) did not show it.

### Example request

```text theme={null}
GET {{base_url}}/api/chapter/40
Accept: application/json
Authorization: Bearer {{token}}
```

### Response — `200 OK`

Called by a student: the draft quiz `78` of chapter 40 is not listed.

```text theme={null}
{
  "id": 40,
  "training_id": 12,
  "label": "Chapter 1 — Fundamentals",
  "position": 1,
  "lessons_count": 3,
  "created_at": "2026-04-01 09:22:10",
  "updated_at": "2026-04-10 14:03:55",
  "archived_at": null,
  "deleted_at": null,
  "quizzes": [
    {
      "id": 77,
      "company_id": 1,
      "training_id": null,
      "chapter_id": 40,
      "instructor_id": 5,
      "label": "Chapter 1 Quiz",
      "slug": "chapter-1-quiz",
      "max_attempts": 3,
      "passing_percentage": 70,
      "duration": 15,
      "is_published": 1,
      "revenue_percentage": 0,
      "...": "other quiz columns, rating / sales fields, instructor",
      "quiz_questions": [
        {
          "id": 300,
          "quiz_id": 77,
          "quiz_question_type_id": 2,
          "label": "What does the project scope define?",
          "tip": null,
          "explanation": "Scope = the work required to deliver the result.",
          "quiz_question_options": [
            { "id": 900, "quiz_question_id": 300, "label": "The work to be done", "is_correct": true, "quiz_question_option_items": [] },
            { "id": 901, "quiz_question_id": 300, "label": "The project budget", "is_correct": false, "quiz_question_option_items": [] }
          ]
        }
      ]
    }
  ],
  "quiz": { "id": 77, "chapter_id": 40, "label": "Chapter 1 Quiz", "...": "same object as quizzes[0] (deprecated)" }
}
```

### Unknown id — `500`

The controller catches the "not found" exception in its generic handler, so an unknown id answers `500` (not `404`) with the model message:

```text theme={null}
{ "message": "No query results for model [App\\Models\\Chapter] 999" }
```

***

## Update a chapter

`POST /chapter/{id}`

Updates an existing chapter. The chapter must belong to the `training_id` supplied in the body — otherwise `404` is returned.

### Body parameters

| Name | Type | Required | Description |
| :- | :- | :- | :- |
| `training_id` | integer | Yes | Must match the chapter's training. |
| `label` | string | Yes | 1–255 characters. |
| `position` | integer | Yes | Minimum `1`. |

### Example request

```shellscript theme={null}
POST {{base_url}}/api/chapter/40
Accept: application/json
Authorization: Bearer {{token}}
Content-Type: application/json
```

```json theme={null}
{
  "training_id": 12,
  "label": "Chapter 1 — Fundamentals (revised)",
  "position": 2
}
```

### Response — `200 OK`

```json theme={null}
{ "message": "Chapter updated successfully." }
```

***

## Permanently delete a chapter

`DELETE /chapter/{id}/hard_delete`

Force-deletes the chapter **and all its lessons**. This is irreversible. The `quizzes.chapter_id` foreign key cascades on delete, so **every quiz of the chapter** is removed by the database too (there can be several since 19/10/2026), together with their questions and the students' attempts on them (those foreign keys cascade as well).

> ⚠️ This endpoint does not soft-delete — both the chapter and every lesson it contains are removed from the database immediately. If you need a recoverable operation, archive upstream (e.g. the training) instead.

### Example request

```text theme={null}
DELETE {{base_url}}/api/chapter/40/hard_delete
Accept: application/json
Authorization: Bearer {{token}}
```

### Response — `200 OK`

```text theme={null}
{ "message": "Chapter deleted successfully." }
```

***

## Postman setup

1. **Environment variables.**
   * `base_url` — your API root, e.g. `http://localhost:8000`
   * `token` — the Sanctum token returned by `POST /auth/login`
2. **Collection-level headers.**
   * `Accept: application/json`
   * `Authorization: Bearer {{token}}`
3. **Request bodies.**
   * JSON bodies → **Body → raw → JSON**
   * For **Reorder**, remember the body is a bare JSON array, not an object wrapped in a key.
4. **Smoke-test flow.**
   1. `POST /chapter` — create
   2. `GET /chapters?training_id={id}` — verify it appears
   3. `POST /chapter/{id}` — update
   4. `POST /chapters/reorder` — reorder with peers
   5. `GET /chapter/{id}` — confirm
   6. `DELETE /chapter/{id}/hard_delete` — cleanup


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.