Skip to main content
Export chapters (with their lessons and all their chapter quizzes) as JSON, download every chapter of a training as a ZIP, import chapters from JSON files, and bulk-import lessons — either uploading their attached images to Cloudflare R2 or to the local Laravel storage. 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
  • Export a single chapter
  • Export all chapters of a training (ZIP)
  • Import chapters from JSON
  • Bulk-import lessons to Cloudflare R2
  • Bulk-import lessons to local storage (deprecated 05-2026)
  • Import behavior at a glance
  • Postman setup

Base URL & Headers

The lesson-import endpoints use multipart/form-data. Do not set an explicit Content-Type header for those requests — let Postman (or your HTTP client) build the multipart boundary itself.

The chapter export schema

Every per-chapter JSON produced by the export endpoints follows this shape (all relations embedded):
The chapter quizzes are exported in two keys (since 19/10/2026, when a chapter started holding several quizzes):
  • quizzes — every quiz of the chapter (published and draft), in creation order, each with its full question tree (questions → options with is_correct → drag-and-drop items). [] when the chapter has none.
  • quiz — ⚠️ deprecated: the same object as quizzes[0] (shortened above), or null. Kept for older readers of export files. Files exported before 19/10/2026 only have quiz.
The file is the full chapter record (Chapter::toArray()), so each quiz also carries its timestamps and the other columns of the quizzes table.

Export a single chapter

Downloads one chapter as a .json file. The file includes the chapter’s training metadata, all lessons, and every quiz of the chapter with its question tree (quizzes, plus the deprecated quiz = the first one).

Query parameters

If either filter doesn’t match, the response is 404.

Example request

In Postman, click Send and Download. The filename pattern is {training-slug}-chapter-{chapter-slug}-{dd-mm-YYYY-HH-mm}.json.

Responses

200 OK — file body follows the chapter export schema. 404 Not Found — chapter does not match the supplied company / training:
422 Unprocessable Entity — missing query parameters:

Export all chapters of a training (ZIP)

GET /chapters/export-all Downloads a ZIP archive containing one JSON file per chapter for the given training.

Query parameters

Example request

ZIP layout

Each JSON file follows the chapter export schema, so every chapter file carries all its quizzes in quizzes.

Responses

404 Not Found — no chapters for the given training:
500 Internal Server Error — ZIP creation failed:

Import chapters from JSON

Creates one or more chapters from uploaded .json files. Lessons found inside the JSON payload are imported along with each chapter. Quizzes are not imported by this endpoint: the quizzes array (and the older quiz key) of an export file is ignored, so an imported chapter starts without quizzes. Recreate them with the Quizzes & Exams endpoints (chapter_id = the new chapter id from the response).

Form-data fields

Per-file JSON validation

  • label — optional (defaults to "Untitled Chapter").
  • position — ignored on import. The new chapter is placed at max(position) + 1 within the target training.
  • lessons — optional array. Each item may include label (defaults to "Untitled Lesson"), type (document or video, defaults to document; unknown values fall back to document), content (defaults to ""), and position (defaults to 0).
  • quizzes / quiz — not imported by this endpoint (ignored when present). The unused quiz-import helper that sat in the controller was removed on 19/10/2026; nothing calls a quiz import from here.

Example file payload

intro-to-pmp-chapter-1.json

Postman setup (Body → form-data)

Multi-file upload:
Single-file upload:

Responses

200 OK — all files succeeded:
200 OK — partial success. Always inspect failed_chapters:
400 Bad Request — nothing uploaded:

Bulk-import lessons to Cloudflare R2

POST /lessons/import_to_r2 Uploads a batch of image files to Cloudflare R2, creates (or reuses) a chapter, and generates one lesson per uploaded file with an <img> tag pointing at the R2 URL. Use this when you want lesson images served from the R2 CDN.

Form-data fields

The training must belong to company_id (a super-admin may name any company’s), else 422. The answer carries folder, the bucket folder the images went to; the Flutter import modal shows it in its success dialog. The whole layout is on R2 Storage Paths.

Behavior notes

  • Lesson order & labels. Lessons are created in the order the files are sent. The first file becomes Lesson 1, the second Lesson 2, and so on.
  • Lesson content. Each lesson’s content is set to <p style="text-align: center;"><img src="{R2 URL}" alt=""></p>.
  • R2 visibility. Files are uploaded with visibility: public so the generated URLs are directly accessible.
  • Lesson type. Every imported lesson is a document (one image per lesson); switch a lesson to video afterwards with POST /lesson/{id}.
  • Transactional. The whole operation runs inside a DB transaction — if any insert fails, both the files and lessons rows are rolled back (the R2 object is not rolled back).

Postman setup (Body → form-data)

Repeat the files key for every file; don’t use files[] with this endpoint — the controller reads $request->file('files').

Response — 200 OK

Error responses

422 Unprocessable Entity — validation error (missing field, or a training of another company):
500 Internal Server Error — R2 (S3) upload failed:
500 Internal Server Error — unexpected error:

Bulk-import lessons to local storage (deprecated 05-2026)

This feature was removed from the UI (May-2026), preferably to use this
Same behavior as the R2 variant above, except files are written to the local Laravel public disk (under storage/app/public/files/{folder}) and the URLs are served from storage/... via the public symlink. Use this when you prefer self-hosted media, or when you’re working in a dev environment without R2 credentials.

Form-data fields

Identical to the R2 variant:

Behavior notes

  • Internally this endpoint delegates file persistence to FilesController@bulk_upload_to_storage. Each file is stored at files/{folder}/{dd-mm-YYYY-HH-mm}-{slugified-name}.{ext}.
  • Each lesson’s content is set to <p style="text-align: center;"><img src="{public URL}" alt=""></p>, the same markup as the R2 variant.
  • The chapter is created (or reused) via firstOrCreate on training_id + label.
  • The whole operation is wrapped in a DB transaction.

Postman setup (Body → form-data)

Response — 200 OK

Error responses

422 Unprocessable Entity:
500 Internal Server Error — underlying bulk upload failed:

Import behavior at a glance


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}} Do not set a Content-Type header on any of the import endpoints on this page — they all use multipart/form-data and need Postman to generate the boundary.
  3. Downloads. For export and export-all, click Send and Download so Postman writes the file to disk.
  4. Uploads — file key naming.
    • Chapter JSON import: chapter_file (single) or chapter_files[] (multiple).
    • Lesson bulk import (both R2 and local): files repeated per file — not files[].
  5. Smoke-test flow.
    1. POST /chapters/import with a single JSON file
    2. GET /chapter/{id}/export?company_id=...&training_id=... — round-trip check
    3. POST /lessons/import_to_storage with a few PNGs — confirm one lesson per image
    4. GET /lessons?chapter_id={id} — verify lessons were created
    5. GET /chapters/export-all?company_id=...&training_id=... — download the full ZIP