---
title: "Add domain memberships to a student"
method: POST
path: "/v2/students/{id}/relationships/domain-memberships/"
tags: ["Students"]
---

# Add domain memberships to a student

`POST /v2/students/{id}/relationships/domain-memberships/`

## Request

- Base URL: `https://api.skilljar.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.skilljar.com/v2/students/{id}/relationships/domain-memberships/`
- Auth: HTTP bearer
- TypeScript: <https://skmtc.dev/skilljar/apis/skilljar-api-v2/docs/v2/students/:id/relationships/domain-memberships/post.ts> (a function with typed inputs; its response is checked against the schema)

Grant the student membership to one or more domains.

The request body is the canonical ``{data: [{type: "domains", id: ...}]}``
envelope — each item is a domain resource identifier (the student is in the
path). This is the (re)activation path (there is no PATCH): a new membership
is created active, an inactive membership (e.g. after a prior DELETE) is
reactivated, and an already-active membership is a **idempotent** no-op —
returned as ``succeeded`` with no separate ``already_exists`` code and no
event. See ``batch_add_domain_memberships`` for the create-vs-reactivate
semantics and the delegation to ``Domain.add_member``.

Within-batch duplicate detection is first-wins on ``id`` (the domain id),
with the JSON:API source pointer at ``/data/{i}/id``. Response is 200 if all
items succeed, 207 if any fail, 400 if the request envelope is empty or over
the cap.

The student lookup runs **before** the envelope guard so a missing student
is reported as 404 regardless of body content.

``exclude_none=True`` matches the batch-envelope convention shared by every
sibling batch endpoint (assets, courses, lessons): null attributes such as an
unset ``expires_at`` are omitted here, whereas the GET list renders them as
``null``. Same resource, two serializers — the drift is inherent to the
batch-vs-list convention across ``api_v2``, not specific to this route.

See ``api_v2/docs/batch-responses.md`` for the envelope shape and
``api_v2/docs/domain-memberships.md`` for the design.

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

## Path parameters

- `id` string, required

## Request body

- DomainMembershipAddRequestEnvelope: JSON:API envelope for POST /v2/students/{id}/relationships/domain-memberships/. The ``data`` field is always an array — single-add is a list of one. Cap enforcement (<= DEFAULT_BATCH_MAX_ITEMS items) lives in the endpoint, NOT here — matches the create/update contract (400 via ValidationError instead of Pydantic 422).
  - `data` DomainRefData[], required: List of domains to grant the student membership to.
    - `type` 'domains', required: Must be "domains".
    - `id` string, required: Obfuscated id of the Domain to grant membership to.

## Response `200`

OK

- BatchResultEnvelopeDomainMembershipResource
  - `data` union[], required: Per-item results in request order.
    - union
      - BatchSucceededItemDomainMembershipResource
        - `status` 'succeeded'
        - `id` string, nullable: Resource ID of the created or updated resource.
        - `result` DomainMembershipResource, required: JSON:API resource object for a DomainMembership join row.
          - `type` 'domain-memberships': Always "domain-memberships".
          - `id` string, required: Obfuscated id of the DomainMembership row.
          - `attributes` DomainMembershipAttributes, required: Attributes of a domain-membership resource object.
            - `active` boolean, required: Whether the membership currently grants access.
            - `expires_at` string, date-time, nullable: Expiry timestamp, or null if it does not expire.
            - `created_at` string, date-time, required: Timestamp when the membership was created.
            - `modified_at` string, date-time, required: Timestamp when the membership was last modified.
          - `relationships` DomainMembershipRelationships, required: Relationships of a domain-membership resource object.
            - `domain` DomainRelationship, required: To-one linkage to the domain the course is published to. Local strict variant — see ``DomainIdentifier`` above.
              - …
          - `links` object, nullable: Self link for the membership.
      - 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' | 'already_in_path', 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
- `406`: Not Acceptable: Accept rules out application/vnd.api+json.
- `413`: Payload Too Large: the request body exceeds the maximum allowed size.
- `415`: Unsupported Media Type: Content-Type must be application/json or application/vnd.api+json.
- `422`: Unprocessable Entity

## Changes

- **2026-10-16** `bed169429eb2`: 2 warning, 3 info
  - added the new `already_in_path` enum value to the `data/items/anyOf[subschema #2: BatchFailedItem]/error/code` response property for the response status `200`
  - added the new `already_in_path` enum value to the `data/items/anyOf[subschema #2: BatchFailedItem]/error/code` response property for the response status `207`
  - added the non-success response with the status `406`
  - added the non-success response with the status `413`
  - …1 more
- **2026-09-15** `9fc34e55336d`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/skilljar/apis/skilljar-api-v2/changes/v2/students/:id/relationships/domain-memberships/post.md)

---

[API](https://skmtc.dev/skilljar/apis/skilljar-api-v2.md) - [All operations](https://skmtc.dev/skilljar/apis/skilljar-api-v2/llms.txt) - [OpenAPI document](https://skmtc.dev/skilljar/apis/skilljar-api-v2/revisions/fbda30ce1742?raw)
