> 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/section-enrollment/update-an-existing-section-enrollment-partial.md).

# Update an existing section enrollment (partial)

Use this endpoint to update a section enrollment and its course enrollment's grades, units, and registration details. It works like `PUT` except for invalid values: when the update proceeds, `PATCH` rejects the whole call with a 400 and writes nothing, while `PUT` stores them as null. `student_username`, `course`, and `section` must match the stored enrollment; to move an enrollment, delete it and create a new one.

## PATCH /planner/enrollment/section/{id}

>

```json
{"openapi":"3.0.0","info":{"title":"Stellic for Developers","version":"1.0"},"tags":[{"name":"Section Enrollment"}],"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":{"PlanCourseCourseSectionRequest":{"allOf":[{"$ref":"#/components/schemas/PlanCourseCourseSection"},{"$ref":"#/components/schemas/CourseVersioningFields"}]},"PlanCourseCourseSection":{"type":"object","properties":{"student_username":{"description":"Student's Stellic username. On PUT and PATCH it must match the stored enrollment.","type":"string"},"course":{"description":"Course code in your catalog. On POST, a well-formed code that isn't in your catalog is added to it. On PUT and PATCH it must match the stored enrollment.","type":"string"},"section":{"description":"Catalog section ID. On PUT and PATCH it must match the stored enrollment.","type":"string"},"term":{"description":"Term ID for the enrollment. Required on POST; ignored on PUT and PATCH.","type":"string"},"is_enrollment_component":{"description":"Whether this section is the enrollment component of the course. On POST, when\nthe course enrollment already exists and this is true, supplied final and\nmid-semester grades fill only missing grades. Valid, nonempty\n`registration_status`, `repeat_code`, `grade_option`, and `enrollment_level`\ncan replace stored values. Units are selected, not added: when values differ,\nthe first nonempty, nonzero value wins, incoming first; identical values are\nretained. If the stored units are null, supply valid nonzero units. Omitting\nunits or sending null when stored units are null returns a 500. The course\nenrollment's timestamp is processed too. `is_transfer` and\n`transfer_university` remain unchanged; use PUT or PATCH to change them.\n\nOn PUT and PATCH, JSON booleans and the strings \"true\" and \"false\" are\nsupported. Prefer JSON booleans. String matching is case-insensitive, but any\nstring other than \"true\", including \"true \", becomes false rather than\nbeing rejected. False bypasses the stale-update skip and still allows\ncourse-level fields to change, but leaves the course enrollment's timestamp\nunchanged.","oneOf":[{"type":"boolean"},{"type":"string"}]},"grade_scheme":{"description":"The grading scheme under which the student received a grade, used with `grade_option` to read `final_grade` and `mid_semester_grade`.","type":"string"},"grade_option":{"description":"The grading option under which the student received a grade for this course. On PUT and PATCH, an empty string is treated as omitted; on an applied update, JSON null clears the stored value.","type":"string","nullable":true},"is_transfer":{"description":"Whether the course was transferred in. On PUT and PATCH, omitting it sets it to `false`.","type":"boolean"},"transfer_university":{"description":"Institution the course was transferred from.","type":"string"},"course_topic":{"description":"Topic the student enrolled in. Used only when POST creates the course enrollment.","type":"string"},"attributes":{"description":"Attribute names on the enrollment. On section-enrollment POST, saved only when\ncreating the course enrollment; an unknown name can create a catalog attribute.\nSection-enrollment PUT and PATCH don't save attributes, but validate newly\nsupplied names when the update proceeds. An unrecognized nonempty name is listed\nin a successful PUT response's `errors[]` and causes a 400 on PATCH. Validate\nattribute names before sending them.","type":"array","items":{"type":"string"}},"registration_status":{"description":"Registration status for the course, for example `ENRL` or `WAIT`. On PUT and PATCH, an empty string is treated as omitted; on an applied update, JSON null clears the stored value. Omit `registration_reason` when clearing the status.","type":"string","nullable":true},"registration_reason":{"description":"Your SIS identifier for the reason behind the registration status. Max 100 characters.\nOn PUT and PATCH, send `registration_status` with it; a reason sent alone is\nignored. A nonempty reason forms the lookup value `{status}#{reason}`, which\nmust exist in your institution's registration-status catalog. An unknown\ncombined value returns a validation 400 on PATCH only if the update proceeds;\na timestamp-driven skip does not validate it. Omit the reason when clearing\nthe status with JSON null.","type":"string"},"repeat_code":{"description":"Repeat code for the course. On PUT and PATCH, an empty string is treated as omitted and leaves the stored value unchanged; on an applied update, JSON null clears it.","type":"string","nullable":true},"final_grade":{"description":"Final grade letter the student received. On PUT and PATCH, the first grade on an enrollment needs `grade_option` or `grade_scheme`.","type":"string"},"mid_semester_grade":{"description":"Mid-semester grade letter the student received.","type":"string"},"units":{"description":"Units the student received.","type":"number"},"enrollment_level":{"description":"Enrollment level for the course, for example `UGRD` or `GRAD`. On PUT and PATCH, an empty string is treated as omitted and leaves the stored value unchanged.","type":"string"},"sis_update_timestamp":{"description":"Your SIS's time for the change, as `YYYY-MM-DDTHH:MM:SS`. On PUT and PATCH,\nan update older than the course enrollment's stored timestamp is skipped\nonly when `is_enrollment_component` is true. On an applied update, the\nsection enrollment's timestamp is replaced with the supplied value, or\ncleared when omitted. The course enrollment's timestamp is replaced or\ncleared only when `is_enrollment_component` is true; false leaves it\nunchanged even when course-level fields change. A malformed nonempty\ntimestamp, including an invalid calendar value, can cause a 500 before\nany enrollment write. Validate it before sending.","type":"string"}},"required":["course","section","is_enrollment_component","student_username"]},"CourseVersioningFields":{"type":"object","description":"Optional fields that link the course enrollment to one version of the course in your catalog.\nTo match a version, send `course_id` and a `course_start_term` that is a known term ID, plus\n`course_offering_number` if your catalog uses offering numbers. If you send none of these\nfields, or `course_start_term` isn't a known term ID, the version recorded on the catalog\nsection is used instead (on the course endpoint, the first listed section that has one); when\nno section has one, the enrollment keeps its current version. Values that match no catalog\nversion leave the current version in place, and no error is returned. Honored by\n`PUT /planner/enrollment/course/{username}/{term_id}`, `POST /planner/enrollment/section/{id}`\n(only when it creates the course enrollment), and `PUT`/`PATCH /planner/enrollment/section/{id}`.\n","properties":{"course_id":{"description":"Your institution's own identifier for the course version (the course ID from your SIS).","type":"string"},"course_offering_number":{"description":"Offering number of the course version, as a string.","type":"string"},"course_start_term":{"description":"Term ID in which this course version starts. If it isn't a known term ID, the other versioning fields are ignored and the section's version is used.","type":"string"}}},"PlanCourseCourseSectionResponse":{"allOf":[{"$ref":"#/components/schemas/PlanCourseResponseFields"},{"type":"object","properties":{"is_enrollment_component":{"description":"Present when a value is stored.","type":"boolean"},"sis_update_timestamp":{"description":"Stored SIS update timestamp of the section enrollment, as `YYYY-MM-DD HH:MM:SS`. Omitted when none is stored.","type":"string"},"section":{"description":"The catalog section.","type":"object","properties":{"id":{"description":"Stellic's internal ID for the catalog section.","type":"integer"},"name":{"description":"Section name.","type":"string"},"campus":{"description":"The catalog section's campus.","type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"screen_name":{"type":"string"}}}}}}}]},"PlanCourseResponseFields":{"type":"object","properties":{"code":{"description":"Course code.","type":"string"},"units":{"description":"Units the student received.","type":"number","nullable":true},"grade":{"description":"Final grade letter.","type":"string","nullable":true},"mid_semester_grade":{"description":"Mid-semester grade letter.","type":"string","nullable":true},"grade_option":{"type":"string","nullable":true},"is_transfer":{"type":"boolean"},"transfer_university":{"type":"string","nullable":true},"registration_status":{"type":"string","nullable":true},"repeat_code":{"type":"string","nullable":true},"attributes":{"description":"Attribute names on the enrollment.","type":"array","items":{"type":"string"}},"tags":{"description":"Names of the enrollment's tags.","type":"array","items":{"type":"string"}},"enrollment_level":{"type":"string","nullable":true},"course_topic":{"description":"Name of the registered topic, or null.","type":"string","nullable":true},"is_residency":{"type":"boolean"},"is_exam":{"type":"boolean"},"cross_course_link_destination_ids":{"description":"Internal IDs you can ignore.","type":"array","items":{"type":"integer"}},"cross_course_link_source_ids":{"description":"Internal IDs you can ignore.","type":"array","items":{"type":"integer"}},"version_id":{"description":"Stellic ID of the linked catalog course version. Present only when the course enrollment is linked to one.","type":"integer"}}}}},"paths":{"/planner/enrollment/section/{id}":{"patch":{"tags":["Section Enrollment"],"parameters":[{"name":"id","in":"path","required":true,"description":"The `{id}` you assigned when you created the section enrollment. Matches only enrolled\nsections on the student's default plan; removed, planned, or other-plan enrollments are\ntreated as not found. May contain `/`. Avoid IDs ending in a slash followed by a\ntimestamp such as `/2024-08-15T10:00:00`, which selects the DELETE-only timestamped route.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanCourseCourseSectionRequest"}}}},"responses":{"200":{"description":"Enrollment updated. `state` is a single object, not an array. On PATCH success,\n`errors[]` is empty; when the update proceeds, per-field validation issues cause\na 400 with the full error list and no write. There is no top-level\n`student_username`, `term`, `grade_scheme`, or `campus` in the response;\n`state.section.campus` is the catalog section's campus.\n\nOn a timestamp-driven skip, the response shape is different: `status: \"success\"`\nwith a `message` field and the stored state, and no `errors` key. Required-field\nchecks, student lookup, enrollment lookup, identifier matching, and timestamp\nparsing run before the skip. Other per-field validation runs only if the update\nproceeds. A skipped 200 therefore does not establish that supplied course-level\nvalues are valid, including a combined registration status and reason.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"state":{"$ref":"#/components/schemas/PlanCourseCourseSectionResponse"},"errors":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Request aborted before any change is written. Unlike PUT, when the update\nproceeds, per-field validation issues (invalid grade, registration status,\nrepeat code, grade option, enrollment level, unknown attribute names) DO\nreturn a 400 on PATCH with the full error list.\nStudent lookup failures return their text in `message` instead of `errors[]`.\n\nCommon causes:\n- Missing or empty request body - returns `section enrollment is a required field` and `Invalid payload`\n- A required body field (`student_username`, `course`, `section`, `is_enrollment_component`) is missing or null - returns `{field} is a required field.`\n- No enrolled section with this `{id}` on the student's default plan (after about 3 seconds of retries) - returns `Section enrollment with id:{id} does not exist`\n- `student_username`, `course`, or `section` doesn't match the stored enrollment, even on an older update - returns `Validating identifier fields failed for {field}. Request body has {incoming_value}, db has {existing_value}`\n- Student does not exist (after retries with exponential backoff) - returns `No student exists with username: {username}`\n- Student is archived - returns `Student with username {username} is archived`\n- On an update that proceeds, invalid `enrollment_level`, `repeat_code`, `registration_status`, `grade_option`, `final_grade`, `mid_semester_grade`, or unknown attribute name - returns the per-field error list"},"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. A malformed nonempty `sis_update_timestamp`, including\nan invalid calendar value, can cause a 500 before any enrollment write on PUT\nand PATCH. This is not covered by the per-field validation 400. Validate the\ntimestamp format and calendar values before sending, and correct a malformed\nvalue before retrying; do not simply resend the same payload.\n\nFor transient server errors, retry with exponential backoff.\n\nCommon causes:\n- Malformed nonempty `sis_update_timestamp`\n- Server error - returns `Stellic couldn't process your request`"}}}}}}
```

#### What happens after the call

* The section enrollment and its course enrollment are updated right away.
* The course enrollment's catalog course version is refreshed when one can be identified.
* Changes to registration status, repeat code, grade option, enrollment level, units, and transfer details appear in the student's activity history shortly after.
* The student's cached audit is invalidated in a background task, so audit and progress views reflect the change once it runs.

#### Notes

* **Validate timestamps before sending.** On PUT and PATCH, a malformed nonempty `sis_update_timestamp`, including an invalid calendar value, can cause an unhandled 500 before any enrollment write. This is not a per-field validation 400. Correct the timestamp before retrying; resending the same malformed value does not fix it.
* **Use JSON booleans.** On PUT and PATCH, prefer JSON `true` and `false` for `is_enrollment_component`. If you use strings, send exactly `"true"` or `"false"` without surrounding whitespace. String matching is case-insensitive, but any string other than `"true"`, including `"true "`, becomes false rather than being rejected. This can bypass the stale-update skip and still change course-level fields.
* **Older updates are skipped only for the enrollment component.** If `is_enrollment_component` is true and the course enrollment's stored `sis_update_timestamp` is newer than yours, nothing changes. If you send false, the update is applied even when its timestamp is older. This applies at every institution, so send your SIS's change time, not the time of the call. A skipped success does not validate your course-level values; validate them before sending.
* **Send values you need to retain.** On an applied update, omitting `is_transfer` sets it to `false`. Omitting `sis_update_timestamp` clears the section enrollment's timestamp and, only when `is_enrollment_component` is true, the course enrollment's timestamp. Sending false still allows course-level fields to change, but leaves the course enrollment's timestamp unchanged. The response includes `state.sis_update_timestamp` only when the section enrollment has a timestamp. After clearing it, handle an absent key rather than JSON `null`, even when `is_enrollment_component: false` leaves a course-level timestamp stored. Other omitted directly updated fields keep their stored values, but catalog course version refresh and automatic topic selection are exceptions. For a topic course, saving can add, replace, or remove the enrollment's topic based on its catalog sections.
* **Empty coded fields leave stored values unchanged.** An empty-string `registration_status`, `repeat_code`, `grade_option`, or `enrollment_level` is treated as omitted, so don't use an empty string to clear one. On an applied update, to clear `registration_status`, `repeat_code`, or `grade_option`, send JSON `null`; omit `registration_reason` when clearing the status.
* **The first grade needs `grade_option` or `grade_scheme`.** If the enrollment has no grade yet, send one of them with `final_grade`.
* **Send the timestamp you intend to retain on a POST merge.** When POST merges into an existing course enrollment with `is_enrollment_component: true`, it overwrites the course enrollment's `sis_update_timestamp` with the supplied timestamp, even an older one, or clears it when omitted. The PUT/PATCH stale-update protection does not apply to this POST merge.
* **`attributes` isn't saved.** Change attributes with [`PUT /planner/enrollment/course/{username}/{term_id}`](/stellic-partner-api/course-enrollments/replace-a-student-s-course-enrollments-for-a-term.md). When replacing a transfer enrollment, include `is_transfer: true` in its PUT block. Omitting it sets the transfer flag to `false` and, when attribute inheritance is enabled, selects section-based rather than course-level inheritance. Include any attributes found only on a catalog course version that you need to retain: course-level inheritance does not retain them automatically. For non-transfer enrollments, when attribute inheritance is enabled, attributes come from the sections in the PUT block, not directly from the catalog course or course version. Include any other enrollment attributes you need to retain; omitted attributes not inherited from those sections can be removed.

#### Related APIs

* [`PUT /planner/enrollment/section/{id}`](/stellic-partner-api/section-enrollment/update-an-existing-section-enrollment-full.md) to update the enrollment and store invalid values as null instead.
* [`GET /planner/enrollment/section/{id}`](/stellic-partner-api/section-enrollment/get-a-section-enrollment.md) to read the enrollment.
* [`POST /planner/enrollment/section/{id}`](/stellic-partner-api/section-enrollment/enroll-a-student-in-a-section.md) to create a section enrollment.
* [`DELETE /planner/enrollment/section/{id}`](/stellic-partner-api/section-enrollment/remove-a-section-enrollment.md) to remove it.
