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

# Quizzes & Exams — Export & Import Endpoints

###### ***Quiz / Exam JSON & ZIP export, bulk question import***

This file documents the **bulk import / export** API surface for Quizzes and Exams — extracted from the main [Quizzes & Exams endpoints](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-endpoints.md) page so it can be consumed on its own. It covers the export of single quizzes/exams to JSON, bulk export of every quiz or exam to a ZIP archive, and the destructive question/option import (with images). All endpoints are served by the `Export_Import_Quizzes_Exams_Controller`.

For the data model, the quiz/exam CRUD endpoints, and authoring questions through the API, see [Quizzes & Exams endpoints](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-endpoints.md). The student attempt lifecycle (start / finish / save score / show answers) is documented in [Quizzes & Exams progress tracking](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-progress-tracking-api.md).

***

## **Table of Contents**

1. **Overview**
2. **Base URL and Headers**
3. **Export / Import Endpoints**
   * **3.1 Export Quiz JSON**
   * **3.2 Import Questions & Options (with images)**
   * **3.3 Export Exam JSON**
   * **3.4 Export All Exams ZIP**
   * **3.5 Export All Quizzes ZIP**
4. **Business Rules & Edge Cases**
5. **History Tracking Actions**
6. **Related Files**

***

## **1. Overview**

A `Quiz` is a container of questions. There is **one Eloquent model and one** `quizzes `**table** behind everything here — what a row *is* depends entirely on whether its `training_id` and `chapter_id` foreign keys are set. There is no `type` column and no separate "exam" table. This distinction decides **which export endpoint you use**:

```text theme={null}
isQuiz()  ⇔  training_id IS NOT NULL  OR  chapter_id IS NOT NULL   → part of a course (chapter quiz OR training quiz)
isExam()  ⇔  training_id IS NULL     AND chapter_id IS NULL        → standalone, sellable
```

* **Quiz** (chapter quiz or training/final quiz) — exported via the `/api/quizzes/...` routes (§3.1, §3.5). Quiz JSON files additionally carry a `belongs_to` block naming the parent training/chapter.
* **Exam** (standalone, both FKs `null`) — exported via the `/api/exams/...` routes (§3.3, §3.4).

Passing an exam id to a quiz export route (or vice-versa) returns `404` — each endpoint filters by its own scope (`isQuiz()` / `isExam()`).

The question/option import (§3.2) is shared by both: it targets any `quizzes` row by id and **destructively replaces** its questions and options.

***

## **2. Base URL and Headers**

Base API URL:

```text theme={null}
{{base_url}}/api
```

Suggested Postman variables:

```text theme={null}
{{base_url}} = http://127.0.0.1:8000
{{token}}    = your_sanctum_token
```

Headers:

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

> `company_id` must be passed explicitly on most endpoints and is validated against the `companies` table. `App\Http\Middleware\CompanyPolicyMiddleware` enforces that the value matches the authenticated user's own `company_id` (super admins bypass this check).

***

## **3. Export / Import Endpoints**

Controller: `app/Http/Controllers/Export_Import_Quizzes_Exams_Controller.php`.

### **3.1 Export Quiz JSON**

`GET /api/quizzes/{quizId}/export-json`

Authorize: `view` on the quiz. Only `isQuiz()` rows (linked to training/chapter) for the authenticated user's `company_id` are exported. Passing an exam id returns `404`.

**Example Request:**

```text theme={null}
GET {{base_url}}/api/quizzes/5/export-json
```

No request body needed. The response is a **file download** (not a JSON object in the response body).

**Response:** streamed `application/json` file download.

* Filename format: `{slug(label)}-quiz-{Y-m-d-H-i}.json`
* Example filename: `chapter-1-final-quiz-2026-05-16-14-30.json`

The downloaded JSON file structure:

```text theme={null}
{
  "id": 5,
  "company_id": 1,
  "training_id": 3,
  "chapter_id": null,
  "label": "Chapter 1 Final Quiz",
  "slug": "chapter-1-final-quiz",
  "passing_percentage": "60.00",
  "duration": 20,
  "is_published": false,
  "belongs_to": {
    "training": { "id": 3, "label": "Web Development Bootcamp", "slug": "web-development-bootcamp" },
    "chapter": null
  },
  "categories": [
    { "id": 1, "label": "Web Development" }
  ],
  "quiz_questions": [
    {
      "id": 21,
      "label": "CSS stands for Cascading Style Sheets.",
      "quiz_question_type": { "id": 1, "label": "True or False" },
      "quiz_question_options": [
        { "id": 60, "label": "True",  "is_correct": true,  "quiz_question_option_items": [] },
        { "id": 61, "label": "False", "is_correct": false, "quiz_question_option_items": [] }
      ]
    }
  ]
}
```

> `belongs_to `**(quiz exports only).** Every exported quiz JSON includes a `belongs_to` block naming the parent training and/or chapter the quiz lives under — so the file is self-describing even outside the database. Resolution:
>
> * **Quiz linked directly to a training** (`training_id` set) → `belongs_to.training` is filled, `belongs_to.chapter` is `null`.
> * **Quiz linked to a chapter** (`chapter_id` set) → `belongs_to.chapter` is filled, and `belongs_to.training` is resolved **through the chapter** (`chapter.training_id`), even when the quiz row itself has no `training_id`.
> * Each entry exposes `id`, `label`, and (for training) `slug`; `chapter` also exposes its `training_id`. This block is **not** present in exam exports (§3.3 / §3.4) — exams are standalone and belong to no training/chapter.

**Error Responses:**

| **Status** | **Reason** |
| :- | :- |
| `404` | Quiz not found, doesn't belong to your company, or it is an exam (use §3.3 instead) |
| `403` | Unauthorized |
| `500` | JSON encoding failure |

***

### **3.2 Import Questions & Options (with images)**

`POST /api/quizzes/{quizId}/import-questions-images`

**Content-Type:** `multipart/form-data` — in Postman, set **Body → form-data**.

**Form-Data Fields:**

| **Key** | **Type in Postman** | **Required** | **Notes** |
| :- | :- | :- | :- |
| `company_id` | Text | **yes** | must match the quiz's `company_id` |
| `questions_file` | File | **yes** | a `.json` file containing the questions |

**Postman Setup:**

1. Set method to `POST`, URL to `{{base_url}}/api/quizzes/5/import-questions-images`
2. Under **Headers**: add `Authorization: Bearer {{token}}` and `Accept: application/json` (do **not** manually set `Content-Type` for form-data — Postman sets it automatically with the boundary)
3. Under **Body**, select **form-data**
4. Add row: Key = `company_id`, Type = Text, Value = `1`
5. Add row: Key = `questions_file`, Type = File, Value = select your `.json` file

**JSON File Content** — save as `questions_import.json` and upload:

```text theme={null}
{
  "quiz_questions": [
    {
      "quiz_question_type_id": 1,
      "label": "Python uses indentation to define code blocks.",
      "tip": null,
      "explanation": "Python relies on indentation instead of braces {}.",
      "image_url": null,
      "quiz_question_options": [
        { "label": "True",  "is_correct": true  },
        { "label": "False", "is_correct": false }
      ]
    },
    {
      "quiz_question_type_id": 2,
      "label": "Which method converts a string to lowercase in Python?",
      "tip": "It is a built-in string method.",
      "explanation": "`str.lower()` returns the string in lowercase.",
      "image_url": null,
      "quiz_question_options": [
        { "label": "lower()",    "is_correct": true  },
        { "label": "toLower()",  "is_correct": false },
        { "label": "downcase()", "is_correct": false }
      ]
    },
    {
      "quiz_question_type_id": 3,
      "label": "Which of the following are mutable data types in Python? (Select all that apply)",
      "tip": null,
      "explanation": "list and dict are mutable; tuple and str are immutable.",
      "image_url": null,
      "quiz_question_options": [
        { "label": "list",  "is_correct": true,  "quiz_question_option_items": [] },
        { "label": "dict",  "is_correct": true,  "quiz_question_option_items": [] },
        { "label": "tuple", "is_correct": false, "quiz_question_option_items": [] },
        { "label": "str",   "is_correct": false, "quiz_question_option_items": [] }
      ]
    },
    {
      "quiz_question_type_id": 4,
      "label": "Match the Python exception to the error it represents.",
      "tip": null,
      "explanation": null,
      "image_url": null,
      "quiz_question_options": [
        {
          "label": "Dividing by zero",
          "is_correct": true,
          "quiz_question_option_items": [
            { "label": "ZeroDivisionError" }
          ]
        },
        {
          "label": "Accessing a non-existent dictionary key",
          "is_correct": true,
          "quiz_question_option_items": [
            { "label": "KeyError" }
          ]
        }
      ]
    }
  ]
}
```

> `image_url` — when provided and not null, the server downloads the image via HTTP and registers it as a `File` record. The URL must be publicly reachable from the server. Set to `null` to skip image import for a question.

**Behavior:**

1. Authorize `update` on the quiz.
2. Verify `company_id` matches the quiz's `company_id`.
3. **Destructive replace** — deletes all existing questions and options on the quiz.
4. Creates questions, options, and option items (drag & drop only) from the file.
5. Downloads any `image_url` values, stores files, and links them to the question.
6. History: `imported_questions_with_images`.

**Success Response (200):**

```text theme={null}
{
  "message": "Questions, options, and images imported successfully.",
  "quiz_id": 5
}
```

**Error Responses:**

| **Status** | **Reason** |
| :- | :- |
| `422` | Invalid JSON, missing required fields, or `company_id` mismatch |
| `403` | Unauthorized |
| `500` | Image download failure or unexpected server error |

***

### **3.3 Export Exam JSON**

`GET /api/exams/export-json`

Both query params are required. The quiz must pass the `isExam()` scope (`training_id = null` and `chapter_id = null`). Passing a quiz (non-exam) id returns `404`.

**Query Parameters:**

| **Param** | **Type** | **Required** |
| :- | :- | :- |
| `company_id` | integer | **yes** |
| `quiz_id` | integer | **yes** |

**Example Request:**

```text theme={null}
GET {{base_url}}/api/exams/export-json?company_id=1&quiz_id=5
```

**Response:** `application/json` file download.

* Filename format: `{slug(label)}-exam-{d-m-Y-H-i}.json`
* Example filename: `advanced-python-exam-16-05-2026-14-30.json`
* File content has the same structure as §3.1, **minus the** `belongs_to `**block** (exams are standalone and belong to no training/chapter).

**Error Responses:**

| **Status** | **Reason** |
| :- | :- |
| `404` | Exam not found, wrong `company_id`, or the quiz has a `training_id`/`chapter_id` (use §3.1 instead) |
| `422` | Missing `company_id` or `quiz_id` |

***

### **3.4 Export All Exams ZIP**

`GET /api/exams/export-all-json`

Exports every `isExam()` row for the company as a single ZIP archive, with one JSON file per exam.

**Query Parameters:**

| **Param** | **Type** | **Required** |
| :- | :- | :- |
| `company_id` | integer | **yes** |

**Example Request:**

```text theme={null}
GET {{base_url}}/api/exams/export-all-json?company_id=1
```

**Response:** ZIP file download.

* ZIP filename format: `exams_export_{d-m-Y-H-i}.zip`
* Each exam inside: `{slug(label)}-exam-{d-m-Y-H-i}.json`

**Error Responses:**

| **Status** | **Reason** |
| :- | :- |
| `404` | No exams found for the given company |
| `422` | Missing `company_id` |
| `500` | ZIP creation or JSON encoding failure |

> Large companies should expect this to be heavy. Make sure your server's `php.ini` allows large uploads: `upload_max_filesize = 300M`, `post_max_size = 300M`, `memory_limit = 512M`, `max_execution_time = 600`.

***

### **3.5 Export All Quizzes ZIP**

`GET /api/quizzes/export-all-json`

Exports every `isQuiz()` row for the company (i.e. quizzes linked to a training and/or a chapter — **not** standalone exams) as a single ZIP archive, with one JSON file per quiz. This is the quiz-side counterpart of §3.4 (Export All Exams ZIP).

**Query Parameters:**

| **Param** | **Type** | **Required** |
| :- | :- | :- |
| `company_id` | integer | **yes** |

**Example Request:**

```text theme={null}
GET {{base_url}}/api/quizzes/export-all-json?company_id=1
```

**Response:** ZIP file download.

* ZIP filename format: `quizzes_export_{d-m-Y-H-i}.zip`
* Each quiz inside: `{slug(label)}-quiz-{d-m-Y-H-i}.json`
* Each JSON file has the same structure as §3.1 — **including the** `belongs_to `**block** that records the parent training and/or chapter the quiz belongs to.

**Error Responses:**

| **Status** | **Reason** |
| :- | :- |
| `404` | No quizzes found for the given company |
| `422` | Missing `company_id` |
| `500` | ZIP creation or JSON encoding failure |

> Like §3.4, this can be heavy for large companies — give PHP enough headroom (`memory_limit = 512M`, `max_execution_time = 600`).

***

## **4. Business Rules & Edge Cases**

* **Scope filtering decides 404s.** Quiz export routes (`/api/quizzes/...`) only match `isQuiz()` rows; exam export routes (`/api/exams/...`) only match `isExam()` rows. Cross-using them returns `404`.
* `belongs_to `**is quiz-only.** Only quiz JSON exports (§3.1, §3.5) carry the `belongs_to` block. For a chapter quiz (only `chapter_id` set), the training is resolved **through the chapter**. Exam exports never include it.
* **Question import is destructive.** §3.2 deletes **all** existing questions and options on the target quiz before re-creating them from the file — there is no merge mode. Back up via §3.1 first if needed.
* `company_id `**must match.** The import verifies the supplied `company_id` equals the quiz's own `company_id` (`422` on mismatch), on top of the tenant `CompanyPolicyMiddleware` check.
* **Image import requires public URLs.** `image_url` values are fetched server-side over HTTP; URLs unreachable from the server cause the import to fail (`500`).
* **Bulk ZIP exports are heavy.** Both §3.4 and §3.5 build the entire archive in memory/temp before streaming; ensure PHP `memory_limit` / `max_execution_time` are generous for large catalogs.

***

## **5. History Tracking Actions**

Calls to `insert_in_history_table($action, $resource_id, $table)` made by this controller:

| **Action** | **Trigger** |
| :- | :- |
| `imported_questions_with_images` | bulk import via JSON + images (§3.2) |

> The export endpoints (§3.1, §3.3, §3.4, §3.5) are read-only and do **not** write history rows.

***

## **6. Related Files**

* `app/Http/Controllers/Export_Import_Quizzes_Exams_Controller.php` — the controller behind every endpoint in this file.
* `routes/api.php` — `Export_Import_Quizzes_Exams_Controller` route group.
* `app/Models/Quiz.php` — parent model (scopes `isQuiz`, `isExam`; `training()` / `chapter()` relations used to build `belongs_to`).
* `app/Models/QuizQuestion.php`, `QuizQuestionType.php`, `QuizQuestionOption.php`, `QuizQuestionOptionItem.php` — the structures serialized into / parsed from the JSON.
* `app/Models/Training.php`, `app/Models/Chapter.php` — sources for the `belongs_to` training/chapter context.
* [Quizzes & Exams endpoints](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-endpoints.md) — data model, quiz/exam CRUD, and question authoring.
* [Quizzes & Exams progress tracking](https://theteam-13fc7f49.mintlify.site/api-reference/quizzes-and-exams-progress-tracking-api.md) — student attempt lifecycle.


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