---
title: "Create students"
method: POST
path: "/v2/students/"
tags: ["Students"]
---

# Create students

`POST /v2/students/`

Create up to BATCH_MAX_ITEMS students in a single request.

The ``data`` array must contain at least one item and at most
``BATCH_MAX_ITEMS`` items. Each item is processed independently — 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.

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

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

**Required OAuth scope:** `students:write`

## Request body

- StudentCreateRequestEnvelope — JSON:API envelope for POST /v2/students/. The ``data`` field is always an array — single-create is a list of one. Cap enforcement (<=200 items) lives in the endpoint, NOT here — that gives us HTTP 400 via ``ValidationError`` instead of Pydantic's 422, per ACCEL-358 acceptance criteria.
  - `data` StudentCreateData[], required — List of student items to create.
    - `type` 'students', required — Must be "students".
    - `attributes` StudentCreateRequest, required — Request schema for creating a new student.
      - `email` string, email, required — Student's email address. Normalized to lowercase on save.
      - `first_name` string — Student's given name. Maximum 50 characters.
      - `last_name` string — Student's family name. Maximum 50 characters.

## Response `200`

OK

- BatchResultEnvelopeStudentResource
  - `data` union[], required — Per-item results in request order.
    - union
      - BatchSucceededItemStudentResource
        - `status` 'succeeded'
        - `id` string, nullable — Resource ID of the created or updated resource.
        - `result` StudentResource, required — JSON:API resource object for a student.
          - `type` string — Always "students".
          - `id` string, required — Opaque student ID. Use in URL paths.
          - `attributes` StudentAttributes, required — Attributes of a student resource object.
            - `email` string, required — Student's email address.
            - `first_name` string, required — Student's given name.
            - `last_name` string, required — Student's family name.
            - `is_inactive` boolean, required — True if the student has been deactivated.
            - `external_id` string, required — Stable UUID5 for cross-system correlation.
            - `date_joined` string, date-time, required — Timestamp when the student joined.
          - `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

---

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