---
title: "Create courses"
method: POST
path: "/v2/courses/"
tags: ["Courses"]
---

# Create courses

`POST /v2/courses/`

Create up to ``BATCH_MAX_ITEMS`` courses in a single request.

Returns 200 if all items succeed, 207 if any fail, 400 if the envelope is
empty or over the cap. Per-row failures (unresolved ``created_by_email``,
unhandled exceptions) do not abort the batch.

No within-batch deduplication is applied — Course has no natural uniqueness
key like the student-email constraint that drives the students endpoint's
first-wins dedup pass.

**Required OAuth scope:** `courses:write`

## Request body

- CourseCreateRequestEnvelope — JSON:API envelope for POST /v2/courses/. The ``data`` field is always an array — single-create is a list of one. Cap enforcement (<= ``BATCH_MAX_ITEMS`` items) lives in the endpoint, NOT here — that gives us HTTP 400 via ``ValidationError`` instead of Pydantic's 422, per the v2 batch-response convention.
  - `data` CourseCreateData[], required — List of course items to create.
    - `type` 'courses', required — Must be "courses".
    - `attributes` CourseCreateRequest, required — Request attributes for POST /v2/courses/.
      - `title` string, required — Course title.
      - `short_description` string — Short description shown on course tiles.
      - `long_description_html` string — Long description as HTML content.
      - `enforce_sequential_navigation` boolean — If true, students must complete the current lesson before accessing the next.
      - `created_by_email` string, email, nullable — Optional dashboard-user email to attribute the create to. Resolved to an active OrganizationMembership in the caller's organization. Omitted/null leaves created_by null.

## Response `200`

OK

- BatchResultEnvelopeCourseResource
  - `data` union[], required — Per-item results in request order.
    - union
      - BatchSucceededItemCourseResource
        - `status` 'succeeded'
        - `id` string, nullable — Resource ID of the created or updated resource.
        - `result` CourseResource, required — JSON:API resource object for a course.
          - `type` string — Always "courses".
          - `id` string, required — Opaque course ID. Use in URL paths.
          - `attributes` CourseAttributes, required — Attributes of a course resource object.
            - `external_id` string, required — Stable UUID5 for cross-system correlation.
            - `title` string, required — Course title.
            - `short_description` string, required — Short description shown on course tiles.
            - `long_description_html` string, required — Long description as HTML content.
            - `enforce_sequential_navigation` boolean, required — If true, students must complete the current lesson before accessing the next.
            - `lesson_count` integer, required — Total number of lessons in the course.
            - `is_published` boolean, required — True if the course is published on at least one live domain.
            - `promo_image_url` string, required — Promo image URL, or "" if no image is set.
            - `average_rating` number, nullable — Average of all student ratings on the org's active rating scale, or null when no ratings exist.
            - `created_at` string, date-time, required — Course creation timestamp.
            - `modified_at` string, date-time, required — Latest update timestamp — max of the course's updated_at and any lesson's updated_at.
          - `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

---

[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)
