---
title: "Update lessons"
method: PATCH
path: "/v2/lessons/"
tags: ["Lessons"]
---

# Update lessons

`PATCH /v2/lessons/`

Update up to BATCH_MAX_ITEMS lessons in a single request.

The ``data`` array must contain at least one item and at most
``BATCH_MAX_ITEMS`` items. Each item must carry the obfuscated lesson ``id``
and an ``attributes`` object with the fields to update. A failure on one
row does not abort the rest. Response is 200 if all items succeed, 207 if
any fail, 400 if the request envelope is empty or over the cap.

See ``api_v2/docs/batch-responses.md`` for the 200/207/400 status convention
and per-item error code vocabulary.

**Required OAuth scope:** `lessons:write`

## Request body

- LessonBatchUpdateRequestEnvelope — JSON:API envelope for PATCH /v2/lessons/ batch updates. The ``data`` field is always an array. Cap enforcement (<=100 items) lives in the endpoint so violations produce HTTP 400 rather than Pydantic 422.
  - `data` LessonBatchUpdateData[], required — List of lesson update items.
    - `type` 'lessons', required — Must be "lessons".
    - `id` string, required — Obfuscated ID of the lesson to update.
    - `attributes` LessonUpdateRequest, required — Request schema for partial updates of a lesson. Fields are typed as non-Optional so Pydantic rejects explicit ``null`` at the type level. PATCH semantics are preserved by Python-level sentinel defaults combined with ``model_fields_set``: an omitted field has its default value but is not in ``model_fields_set``, so callers use ``model_dump(exclude_unset=True)`` to forward only fields the client explicitly provided. The sentinel default is stripped from the emitted JSON Schema so SDK generators do not advertise it to clients. The lesson ``type`` field is read-only on update and is intentionally not part of this schema; clients that send it have it silently ignored.
      - `title` string — New lesson title. Must be non-empty when provided.
      - `description_html` string — New HTML description shown above the lesson body. Empty string ``""`` is accepted and clears the description, mirroring the create-side default. This is intentional asymmetry with ``title`` and ``content_html`` (which reject empty/whitespace) — the description is genuinely optional, the title and body are not.
      - `content_html` string, nullable — New HTML body of the lesson. Only valid on HTML-type lessons; attempting to set this on a non-HTML lesson returns 400.
      - `order` integer — New ordering value within the parent course. Order conflicts with sibling lessons are not auto-resolved: if the requested value collides with another lesson in the same course, the update succeeds and both lessons retain the same value; display order in that case is undefined.
      - `content_items` ContentItemRequest[], nullable — Authoritative ordered content blocks for a MODULAR lesson. Omitted = children untouched; present non-empty = diff/reorder; present ``[]`` = delete all children. Capped at 15 items; at most one QUIZ and one RATING item. Valid only on MODULAR lessons — the cross-state MODULAR gate is enforced by the service.
        - `id` string, nullable — Obfuscated content-item id. Present = update existing child; omitted = create new.
        - `type` 'HTML' | 'VIDEO' | 'AUDIO' | 'QUIZ' | 'RATING', required — Content item type.
        - `header` string — Optional header shown above the block.
        - `content_html` string, nullable — HTML body. Only valid when type is "HTML".
        - `asset_id` string, nullable — Obfuscated asset id. Only valid when type is "VIDEO" or "AUDIO".
        - `quiz_id` string, nullable — Obfuscated quiz id. Only valid when type is "QUIZ".
        - `rating` RatingConfig — Rating configuration carried by a ``RATING`` content item. ``rating_scale`` is a **proxy** for the course-level ``Course.rating_scale`` (not a ``RatingContentBlock`` column), mirroring ``api/v1/lessons/rating_content_blocks/serializers.py``. Constrained to ``5`` / ``10`` at the type level so an out-of-range value (e.g. ``7``) is a 422; the 10-point capability gate is enforced in the service. Unknown keys are rejected (422) so a typo'd rating field never silently no-ops. Reused as the read shape on ``ContentItemResource``: the builder echoes the effective course ``rating_scale`` and omits the low/high labels when the 10-point flag is off (relying on the endpoint's ``exclude_none``).
          - `rating_question` string, nullable — Main rating question shown to learners.
          - `additional_feedback_question` string, nullable — Optional follow-up feedback question.
          - `include_additional_feedback` boolean, nullable — Whether to show the feedback question.
          - `rating_scale_low_label` string, nullable — Low-end label (10-point scale only).
          - `rating_scale_high_label` string, nullable — High-end label (10-point scale only).
          - `rating_scale` 5 | 10, nullable — Rating scale (5 or 10). Course-level proxy — stored on Course, not the block.
      - `quiz_id` string — New quiz reference for a QUIZ lesson. Must be non-empty when provided; re-points the lesson at a different quiz. There is no null-clear affordance — a QUIZ lesson's quiz cannot be removed via PATCH. Only valid on QUIZ lessons; the cross-state QUIZ gate is enforced by the service.

## Response `200`

OK

- BatchResultEnvelopeLessonDetailResource
  - `data` union[], required — Per-item results in request order.
    - union
      - BatchSucceededItemLessonDetailResource
        - `status` 'succeeded'
        - `id` string, nullable — Resource ID of the created or updated resource.
        - `result` LessonDetailResource, required — JSON:API resource object for a lesson on the detail/create/update path. Carries ``content_items`` (via ``LessonDetailAttributes``). The **list** route keeps returning the plain ``LessonResource`` so its shape is unchanged.
          - `type` 'lessons' — Always "lessons".
          - `id` string, required — Opaque lesson ID. Use in URL paths.
          - `attributes` LessonDetailAttributes, required — Attributes of a lesson resource on the detail/create/update responses. Extends the list attributes with the MODULAR ``content_items`` array. Kept off the shared ``LessonAttributes`` so the **list** response stays byte-identical (an Optional ``content_items`` on the list type would serialize as ``content_items: null`` — the list route has no ``exclude_none``). ``None`` for non-MODULAR lessons; the detail/create/update routes set ``exclude_none`` so the field is omitted then.
            - `title` string, required — Lesson title.
            - `type` string, required — Lesson type code (e.g. HTML).
            - `order` integer, required — Ordering value within the parent course.
            - `course_id` string, required — Obfuscated ID of the parent course.
            - `description_html` string — HTML description shown above the lesson body.
            - `content_html` string, nullable — HTML body of the lesson (when type="HTML").
            - `quiz_id` string, nullable — Obfuscated ID of the referenced quiz (when type="QUIZ").
            - `external_id` string, required — Stable UUID5 for cross-system correlation.
            - `created_at` string, date-time, required — Timestamp when the lesson was created.
            - `modified_at` string, date-time, required — Timestamp when the lesson was last modified.
            - `content_items` ContentItemResource[], nullable — Ordered content blocks. Present only when type is "MODULAR".
              - …
          - `relationships` object, nullable — Related resource links.
          - `links` object, nullable — Self and related links.
      - BatchFailedItem — Wrapper for a failed item in a 207 batch response.
        - `status` 'failed'
        - `id` string, nullable — Echoed resource ID if the input identified a row.
        - `error` BatchItemError, required — Per-item error inside a 207 batch response. Aligned with JSON:API ``ErrorObject`` field naming (``code`` rather than ``reason``) so consumers can reuse error-handling logic across document-level errors (``ErrorObject`` in ``ErrorEnvelope``) and per-item errors (here). Differences from ``ErrorObject``: ``status``/``title`` are omitted because they're redundant for the 207-batch context (HTTP status is on the envelope, and the title is derivable from ``code``).
          - `code` 'duplicate_email' | 'duplicate_in_batch' | 'duplicate_name' | 'validation_error' | 'not_found' | 'internal_error' | 'not_in_domain' | 'already_enrolled' | 'already_published', required — Machine-readable error code.
          - `source` object, nullable — Pointer to the offending input slot, e.g. {"pointer": "/data/0/attributes/email"}.
          - `detail` string, nullable — Human-readable explanation of the error.
  - `summary` BatchSummary, required — Aggregate counts for a 207 batch response. Invariant: ``succeeded + failed == total``. Enforced by ``@model_validator``.
    - `total` integer, required — Total number of items submitted.
    - `succeeded` integer, required — Number of items that succeeded.
    - `failed` integer, required — Number of items that failed.

## Other responses

- `207` — Multi-Status
- `400` — Bad Request
- `401` — Unauthorized
- `403` — Forbidden
- `422` — Unprocessable Entity
- `500` — Internal Server Error

---

[API](https://skmtc.net/skilljar/apis/skilljar-api-v2.md) · [All operations](https://skmtc.net/skilljar/apis/skilljar-api-v2/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/skilljar/skilljar-api-v2/revisions/f8025de20169/schema)
