---
title: "Bulk-enroll students"
method: POST
path: "/v2/enrollments/"
tags: ["Enrollments"]
---

# Bulk-enroll students

`POST /v2/enrollments/`

Enroll up to ``BATCH_MAX_ITEMS`` students into one published course.

The hybrid envelope shares ``published_course_id`` and ``expires_at`` at the
top level; ``data`` is the per-row item list. Each row is processed
independently — a failure on one row does not abort the rest. Response is
200 if every row succeeded, 207 if any row failed, 400 if the request
envelope is empty, over the cap, or carries a past ``expires_at`` (matches
the ticket AC "past timestamps return 400, request-level").

Within-batch duplicate detection is first-wins: subsequent occurrences of
an email are pre-marked ``duplicate_in_batch`` without reaching the service.

See ``api_v2/docs/enrollments.md`` for the per-row failure-code vocabulary,
reactivation behavior, retry contract, and demo seed prerequisites.

**Required OAuth scope:** `enrollments:write`

## Request body

- EnrollmentBatchCreateRequest — Hybrid JSON:API envelope for POST /v2/enrollments/. Top-level ``published_course_id`` and ``expires_at`` are shared across every row in ``data``. Per-row repetition is omitted by design (documented Pulse epic exception). Cap enforcement and ``expires_at`` future-validation live in the endpoint — not here — so they surface as HTTP 400 via ``ValidationError``, matching the ticket AC ("past timestamps return 400, request-level validation").
  - `published_course_id` string, required — Obfuscated id of the target PublishedCourse.
  - `expires_at` string, date-time, nullable — ISO 8601 timestamp when the enrollments expire. Optional. Must be in the future when provided.
  - `data` EnrollmentCreateData[], required — List of enrollment items to create.
    - `type` 'enrollments', required — Must be "enrollments".
    - `attributes` EnrollmentCreateRequest, required — Per-row attributes for bulk-enroll.
      - `email` string, email, required — Student's email address. Normalized to lowercase server-side.

## Response `200`

OK

- BatchResultEnvelopeEnrollmentWriteResource
  - `data` union[], required — Per-item results in request order.
    - union
      - BatchSucceededItemEnrollmentWriteResource
        - `status` 'succeeded'
        - `id` string, nullable — Resource ID of the created or updated resource.
        - `result` EnrollmentWriteResource, required — JSON:API resource object for an enrollment.
          - `type` string — Always "enrollments".
          - `id` string, required — Opaque enrollment ID.
          - `attributes` EnrollmentWriteAttributes, required — Attributes of the bulk-enroll (write) response resource. ``channel`` is intentionally absent from this write shape — it's an internal source-of-enrollment dimension that Pulse doesn't consume.
            - `enrolled_at` string, date-time, required — Timestamp when the student was enrolled.
            - `expires_at` string, date-time, nullable — Timestamp when access expires, or null for no expiry.
            - `due_at` string, date-time, nullable — Timestamp the course is due, or null when no due date.
            - `active` boolean, required — Whether the enrollment is currently active.
          - `relationships` object, nullable — Related student and published-course links.
          - `links` object, nullable — Self link.
      - 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
- `404` — Not Found
- `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)
