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 usemultipart/form-data. Do not set an explicitContent-Typeheader 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):quizzes— every quiz of the chapter (published and draft), in creation order, each with its full question tree (questions → options withis_correct→ drag-and-drop items).[]when the chapter has none.quiz— ⚠️ deprecated: the same object asquizzes[0](shortened above), ornull. Kept for older readers of export files. Files exported before 19/10/2026 only havequiz.
Chapter::toArray()), so each quiz also carries its timestamps and the other columns of the quizzes table.
Export a single chapter
.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
quizzes.
Responses
404 Not Found — no chapters for the given training:
500 Internal Server Error — ZIP creation failed:
Import chapters from JSON
.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 atmax(position) + 1within the target training.lessons— optional array. Each item may includelabel(defaults to"Untitled Lesson"),type(documentorvideo, defaults todocument; unknown values fall back todocument),content(defaults to""), andposition(defaults to0).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: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 secondLesson 2, and so on. - Lesson content. Each lesson’s
contentis set to<p style="text-align: center;"><img src="{R2 URL}" alt=""></p>. - R2 visibility. Files are uploaded with
visibility: publicso the generated URLs are directly accessible. - Lesson type. Every imported lesson is a
document(one image per lesson); switch a lesson tovideoafterwards withPOST /lesson/{id}. - Transactional. The whole operation runs inside a DB transaction — if any insert fails, both the
filesandlessonsrows are rolled back (the R2 object is not rolled back).
Postman setup (Body → form-data)
Repeat thefileskey for every file; don’t usefiles[]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 thispublic 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 atfiles/{folder}/{dd-mm-YYYY-HH-mm}-{slugified-name}.{ext}. - Each lesson’s
contentis 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
firstOrCreateontraining_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
- Environment variables.
base_url— your API root, e.g.http://localhost:8000token— the Sanctum token returned byPOST /auth/login
- Collection-level headers.
Accept: application/jsonAuthorization: Bearer {{token}}Do not set aContent-Typeheader on any of the import endpoints on this page — they all usemultipart/form-dataand need Postman to generate the boundary.
- Downloads. For
exportandexport-all, click Send and Download so Postman writes the file to disk. - Uploads — file key naming.
- Chapter JSON import:
chapter_file(single) orchapter_files[](multiple). - Lesson bulk import (both R2 and local):
filesrepeated per file — notfiles[].
- Chapter JSON import:
- Smoke-test flow.
POST /chapters/importwith a single JSON fileGET /chapter/{id}/export?company_id=...&training_id=...— round-trip checkPOST /lessons/import_to_storagewith a few PNGs — confirm one lesson per imageGET /lessons?chapter_id={id}— verify lessons were createdGET /chapters/export-all?company_id=...&training_id=...— download the full ZIP