> For the complete documentation index, see [llms.txt](https://docs.stellic.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.stellic.com/stellic-partner-api/program-enrollments/enroll-a-student-in-a-program.md).

# Enroll a student in a program

Use this endpoint when a student declares a major, minor, certificate, or other program. The enrollment is created on the student's default plan and matched to a published audit for the student, program, and catalog year. If the program is already on the student's plan as an undeclared program, the call declares it instead of creating a new record.

## POST /planner/enrollment/program/{username}/{program\_id}

>

```json
{"openapi":"3.0.0","info":{"title":"Stellic for Developers","version":"1.0"},"tags":[{"name":"Program Enrollments"}],"servers":[{"url":"https://{institution}.stellic.com/partnerapi/v1","variables":{"institution":{"default":"{institution}","description":"Your institution's Stellic subdomain: the part before `.stellic.com` in the URL you use to sign in. It becomes the host in every request, as in `https://{institution}.stellic.com/partnerapi/v1/...`. Replace the `{institution}` placeholder with your subdomain before sending."}}}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Personal Access Token issued by your Stellic integration partner. Send it as `Authorization: Bearer <token>` on every request. One token per institution; do not share across environments."}},"schemas":{"StudentProgramRequest":{"type":"object","properties":{"certification_status":{"type":"string","enum":["true","false"],"description":"Whether the student has completed (graduated from) this program. Send a string: `\"true\"` (case-insensitive) certifies, and any other string, including `\"false\"`, does not. A JSON boolean is accepted but always read as `false`, so boolean `true` does not certify. A non-zero number returns a 500."},"certification_date":{"type":"string","format":"date","description":"Certification date in `YYYY-MM-DD` format. On a new enrollment it is saved as sent; when the call updates an existing program (`PATCH`, or `POST` declaring an undeclared program), it is saved only if the same call certifies. A malformed value in a call that would save it returns a 500 and nothing is saved; otherwise it is a warning in `errors[]` on a 200. Validate dates before sending. The response renders this field as `Month DD, YYYY` with a zero-padded day (for example `\"May 05, 2026\"`) or `null`, not ISO."},"order":{"type":"integer","nullable":true,"description":"The order in which program requirements are displayed and processed in the audit for this student. Optional; if omitted, stored as `null` (not `0`). Final audit position also depends on certification status, diploma order, program type, primary-major status, and auto-order rules, so treat this as a hint."},"catalog_term":{"description":"SIS term code (as sent in your data sync, for example `fall-2024`) that pins the catalog year used to select the audit version for this program enrollment. Saved on both `POST` and `PATCH`; a changed value re-matches the student's programs to published audits, except certified or locked ones. Send `null` to clear it. An unknown code is a warning in `errors[]` and leaves the stored value unchanged. Not related to section enrollment.","type":"string"},"concentration_ids":{"type":"array","description":"List of concentration `short_name` values to link to this enrollment. Additive on this endpoint; `PATCH` cannot remove concentrations later. IDs that don't belong to this program are reported as warnings in `errors[]`.","items":{"type":"string"}}}},"StudentProgramResponseSuccess":{"type":"object","properties":{"status":{"type":"string"},"state":{"type":"object","properties":{"program":{"type":"string","description":"The program's `short_name`, as passed in the path."},"certification_status":{"type":"boolean"},"certification_date":{"type":"string","nullable":true,"description":"Rendered as `Month DD, YYYY` with a zero-padded day (for example `May 05, 2026`), not ISO."},"order":{"type":"integer","nullable":true},"concentrations":{"type":"array","description":"Every concentration linked to this enrollment, including ones the student has planned but isn't enrolled in (`enrolled` false).","items":{"type":"object","properties":{"name":{"type":"string"},"enrolled":{"type":"boolean"},"planned":{"type":"boolean"},"is_internal":{"type":"boolean"},"is_archived":{"type":"boolean"},"concentration_type":{"type":"object","properties":{"short_name":{"type":"string"},"name":{"type":"string"}}}}}},"units_label":{"type":"object","description":"Present only when the program is configured with a custom units label.","properties":{"field_name":{"type":"string"},"label_plural":{"type":"string"},"label_singular":{"type":"string"}}}}},"errors":{"type":"array","description":"Warning strings for optional fields that were skipped (`POST` and `PATCH` only; the `GET` response has no `errors` key).","items":{"type":"string"}}}},"StudentLinkResponseFailure":{"allOf":[{"$ref":"#/components/schemas/GenericResponseFailure"},{"type":"object","properties":{"identifiers":{"type":"object","properties":{"student_username":{"type":"string"}}}}}]},"GenericResponseFailure":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"}}}}},"paths":{"/planner/enrollment/program/{username}/{program_id}":{"post":{"tags":["Program Enrollments"],"parameters":[{"name":"username","in":"path","required":true,"description":"Student's username. The endpoint does not lowercase or case-normalize it.","schema":{"type":"string"}},{"name":"program_id","in":"path","required":true,"description":"The program's short name, as sent in your data sync. Matched exactly; case-sensitive.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"A request body is required. An empty `{}` is rejected with 400. Every individual field inside the body is optional, but at least the body itself must be present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudentProgramRequest"}}}},"responses":{"200":{"description":"Enrollment created, or, if the program was on the student's plan as an undeclared\nprogram, that program declared. Inspect the `errors` array; a non-empty array means\noptional fields (unknown concentration, unknown `catalog_term`, or a malformed\n`certification_date` that this call doesn't save) were skipped while the enrollment\nitself succeeded. `state.certification_date` is rendered as `Month DD, YYYY` with a\nzero-padded day (for example `\"May 05, 2026\"`) or `null`, not ISO. `state.order` may be\n`null`. Concentration entries have `name`, `enrolled`, `planned`, `is_internal`,\n`is_archived`, and a nested `concentration_type` (`short_name`, `name`); the\nconcentration `short_name` you sent does not appear at the top of the entry.\n`state.units_label` is present only for programs configured with a custom units label.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudentProgramResponseSuccess"}}}},"400":{"description":"Hard validation failure. Typical causes: student or program doesn't exist, student\nalready enrolled in this program, empty body. An unknown concentration or\n`catalog_term` does NOT return 400; it is a warning in a 200. A malformed\n`certification_date` on a new enrollment returns a 500, not a 400.\n\nCommon causes:\n- No non-test student matches the username, including when only a test student account matches - returns `No student exists with username: {username}`. Test student accounts cannot be enrolled, even when archived.\n- Program does not exist - returns `No program exists with unique identifier: {program_id}`\n- Empty or missing request body - returns `request body with student program information is required.`\n- Student already enrolled in this program - returns `{username} is already enrolled into program {program_name}`\n- Matching non-test student is archived - returns `Student with username {username} is archived`\n- Program is a compliance requirement (compliance requirements are not programs a student can be enrolled in through the API) - returns `Compliance requirements are not available via the API: {program_id}`","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/StudentLinkResponseFailure"},{"type":"object","properties":{"identifiers":{"type":"object","properties":{"student_username":{"type":"string"},"program_id":{"type":"string"}}}}}]}}}},"401":{"description":"Missing or invalid Bearer token.\n\nCommon causes:\n- Unauthenticated / invalid token"},"403":{"description":"Your IP address isn't on the allowlist for this endpoint. Contact Stellic to add it; the response includes the `client_ip` Stellic saw.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"client_ip":{"type":"string"}}}}}},"500":{"description":"Unhandled server error. Retry with exponential backoff.\n\nCommon causes:\n- Unhandled server error - returns `Stellic couldn't process your request`\n- Malformed `certification_date` on a new enrollment, or on a call that declares an undeclared program and certifies it - returns `Stellic couldn't process your request`; nothing is saved\n- `certification_status` sent as a non-zero number - returns a 500; send the string `\"true\"` or `\"false\"`"}}}}}}
```

#### What happens after the call

* The enrollment is created right away, with any valid concentrations linked.
* A published audit matching the student, program, and `catalog_term` is attached right away. If none matches, the program has no audit until a matching one is published.
* The student's other programs are re-matched to published audits in the same request, except certified or locked ones, so this call can move another program to a different audit version.
* The student's cached audit is invalidated right away. It can be invalidated again after general requirements are recalculated in the background, even if a matching general requirement was already on the plan and its stored values didn't change.
* General requirements and program order are recalculated in a background task. If your institution uses the pathway experience, auto-applied pathways are re-evaluated after that. This can apply newly matching pathways or remove previously auto-applied ones, but updates may be skipped or fail. The enrollment response does not report the pathway outcome; review the student's pathways to confirm it. For example, a pathway using a draft or in-review audit cannot be applied until the audit is published. A pathway the student opted out of is not automatically re-applied. When complex pathway matching is enabled, opting out of an exact multi-program match can also prevent another matching pathway from being automatically selected. For ties among top matches for a major, an already-applied tied pathway is kept; otherwise the candidates are presented for a choice. When complex pathway matching is enabled, the first exact multi-program match may be selected without presenting equally matching alternatives or keeping a different already-applied pathway.
* The student's disabled status is cleared right away, regardless of activation selection. An inactive login account is activated only if selected under your institution's setup. Ask your Stellic Representative how students are selected.
* The enrollment appears in the student's activity history shortly after.

#### Notes

{% hint style="warning" %}
**Send `certification_status` as the string `"true"` to certify.** A JSON boolean `true` is read as `false`. On a new enrollment that leaves the program uncertified; when the call declares an undeclared program, it removes any certification that program had.
{% endhint %}

* **A malformed `certification_date` fails a new enrollment.** Any value other than `YYYY-MM-DD` returns a 500 and nothing is saved, so validate dates before sending.
* **Declaring an undeclared program keeps its stored values for fields you omit.** Send every field you want set, not only the ones that changed.
* **The primary major isn't set here.** New enrollments are never marked primary, and no Partner API endpoint sets it; it comes from your data sync.
* **Concentrations can't be removed later.** To drop one, delete the enrollment and enroll again with the final `concentration_ids`. Response entries don't include the concentration `short_name`, so match them on `name`.
* **Schools, departments, and advisor access aren't updated.** If advisors see students by school or department, send the student's full profile, including `school_ids` and `department_ids`, with `PUT /student/{student_username}/profile/`, which also refreshes advisor access.

#### Related APIs

* [`GET /planner/enrollment/program/{username}/{program_id}`](/stellic-partner-api/program-enrollments/get-a-student-s-program-enrollment.md) to read the enrollment.
* [`PATCH /planner/enrollment/program/{username}/{program_id}`](/stellic-partner-api/program-enrollments/update-fields-on-an-existing-program-enrollment.md) to certify, reorder, change the catalog year, or add concentrations.
* [`DELETE /planner/enrollment/program/{username}/{program_id}`](/stellic-partner-api/program-enrollments/remove-a-student-s-program-enrollment.md) to remove the enrollment.
* [`PUT /student/{student_username}/profile/`](/stellic-partner-api/student/upsert-a-student-profile.md) to update schools and departments and refresh advisor access.
