> 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/student/upsert-a-student-profile.md).

# Upsert a student profile

Use this endpoint to create a student, or to update one with their full profile, without first checking whether they exist. Every call must include `first_name`, `last_name`, `email`, `term_joined`, and `class_level_id`, plus `student_id` at institutions that identify students by something other than their username. Creates and updates both return 200 with the same body.

## PUT /student/{student\_username}/profile/

>

```json
{"openapi":"3.0.0","info":{"title":"Stellic for Developers","version":"1.0"},"tags":[{"name":"Student"}],"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":{"StudentProfileCreationRequestBody":{"type":"object","properties":{"first_name":{"type":"string","description":"First name of the student."},"preferred_first_name":{"type":"string","description":"When sent, saved as the student's first name in place of `first_name`."},"last_name":{"type":"string","description":"Last name of the student."},"email":{"type":"string","description":"Email address of the student. Lowercased before it's saved."},"legal_name":{"type":"string","description":"Legal name of the student. On an existing student, omitting it clears it."},"primary_advisor_username":{"type":"string","description":"Username of the primary advisor. Lowercased unless your institution signs users in with an identifier other than the username, in which case the case you send must match the advisor's record. An unknown username creates an advisor account only after validation passes, inside the profile-write transaction. Validation failures do not create the account; a failure inside that transaction rolls its creation back. Omitting it keeps the current advisor; you can't clear the advisor through this endpoint."},"primary_campus_id":{"type":"string","description":"The primary campus's name, lowercased before it's matched. On an existing student, omitting it clears the primary campus. A value that doesn't match leaves the existing primary campus unchanged and is reported as `invalid primary_campus_id value` in `errors`; other valid changes are saved on an otherwise successful call."},"current_campus_id":{"type":"string","description":"The campus name for the student's current term, lowercased before it's matched. Saved only when it differs from the primary campus."},"pronouns":{"type":"string","description":"Pronouns of the student. On an existing student, omitting it clears it."},"department_ids":{"type":"array","items":{"type":"string"},"description":"Department short names. Replaces the student's full set; an entry that doesn't match is reported in `errors` and that link is removed. Omit the key to keep the current set."},"school_ids":{"type":"array","items":{"type":"string"},"description":"School short names. Replaces the student's full set; an entry that doesn't match is reported in `errors` and that link is removed. Omit the key to keep the current set."},"pre_req_waivers":{"type":"array","items":{"type":"string"},"description":"Course codes of the courses whose prerequisites are waived for the student. Replaces the full set. Saved on both creates and updates."},"permission_section_ids":{"type":"array","items":{"type":"string"},"description":"Catalog IDs of the sections the student can register for past section restrictions. Saved on both creates and updates. A valid list replaces the student's full set. If any ID doesn't match a section, the whole list is ignored and the student's permission sections remain unchanged; each unmatched ID is reported as `{value} is an invalid permission_section_ids value` in `errors`. Other valid profile changes are still saved on an otherwise successful call. Omit the key to keep the current set, even on PUT; send `[]` to clear it."},"class_level_id":{"type":"string","description":"The class level's short name. Saved on both creates and updates. A value that doesn't match is reported in `errors` and the student is created with no class level."},"type":{"type":"string","description":"The student type's name. Saved on both creates and updates. A value that doesn't match is reported in `errors`."},"primary_enrollment_level_id":{"type":"string","description":"The enrollment level's name. Saved on both creates and updates."},"term_joined":{"type":"string","description":"The SIS term code of the student's entry term. Sets the student's entry year. A code that doesn't match a term returns a 400. When the entry year changes, retained courses and placeholders keep their calendar terms and their relative study-year positions are recalculated during the call. Unregistered courses in past calendar terms and past-term placeholders are removed; check the student's plan before making this change."},"catalog_term":{"type":"string","nullable":true,"description":"The SIS term code of the student's catalog term. A nonempty code that doesn't match a term is reported in `errors` and preserves the stored catalog term on an otherwise successful update. Send `null` or an empty string to clear it."},"ge_catalog_term":{"type":"string","nullable":true,"description":"Alias for `catalog_term`. When both are sent, `ge_catalog_term` is used."},"expected_graduation":{"type":"string","nullable":true,"description":"The SIS term code of the student's expected graduation term. Saved on both creates and updates. A nonempty code that doesn't match a term is reported in `errors` and preserves the stored term on an otherwise successful update. Send `null` or an empty string to clear it."},"year_begin":{"type":"integer","description":"Not used on PUT. The entry year always comes from `term_joined`."},"student_id":{"type":"string","description":"Student ID. Written on every PUT, create or update; omit it and the stored ID is cleared. Required when your institution's learning management system integration or single sign-on identifies students by student ID rather than username."},"uni_id":{"type":"string","description":"On an existing student, writes the same student ID as `student_id`. If both keys are sent, the later key in the request body wins; if `student_id` is omitted, the student ID is cleared. Ignored when the student is created."},"sis_id":{"type":"string","nullable":true,"maxLength":30,"description":"The identifier your learning management system integration matches the student on. On creation, a sent `sis_id` is saved as sent, including an empty string; `null` saves an empty string. When omitted, it's set from the username or `student_id`, depending on how your institution is set up. On updates, omitting it preserves the stored value unless a nonempty `student_id` updates it at institutions that use student IDs for learning management system matching. If both keys are sent, `sis_id` wins. A `null` clears it to an empty string. A non-string value, or one longer than 30 characters, returns a 400. Ask your Stellic Representative how your institution is set up."},"mobile_number":{"type":"string","description":"Mobile number of the student. Saved only when `text_consent` is `true` in the same body; otherwise the stored number is cleared."},"text_consent":{"oneOf":[{"type":"boolean","nullable":true},{"type":"string"}],"description":"Consent to text messages, which `mobile_number` requires. Only boolean `true` or the string `\"true\"` (case-insensitive) grants consent; other boolean or string values and `null` do not."}},"required":["first_name","last_name","email","class_level_id","term_joined"]},"StudentProfileCreationSuccess":{"type":"object","properties":{"status":{"type":"string"},"state":{"type":"object","properties":{"username":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":"string"},"primary_advisor":{"type":"string","nullable":true,"description":"Username of the primary advisor, or `null` if unset."},"pronouns":{"type":"string","nullable":true},"departments":{"type":"array","items":{"type":"string"},"description":"Short names of the departments currently linked to the student."},"schools":{"type":"array","items":{"type":"string"},"description":"Short names of the schools currently linked to the student."},"year_begin":{"type":"integer","nullable":true,"description":"The student's entry year."},"campus":{"type":"string","nullable":true,"description":"The primary campus's display name, or `null` if unset."},"expected_graduation":{"type":"string","nullable":true,"description":"SIS term code of the expected graduation term (for example `spring-2028`), or `null` if unset."},"term_joined":{"type":"string","nullable":true,"description":"SIS term code of the entry term, or `null` if unset."},"catalog_term":{"type":"string","nullable":true,"description":"SIS term code of the catalog term, or `null` if unset."},"student_id":{"type":"string","nullable":true,"description":"The student ID shown in Stellic."},"sis_id":{"type":"string","description":"The identifier your learning management system integration matches the student on."},"mobile_number":{"type":"string","nullable":true},"permission_section_ids":{"type":"array","items":{"type":"string"},"description":"Catalog IDs of the sections the student can register for past section restrictions."}}},"errors":{"type":"array","items":{"type":"string"},"description":"One plain-string message per value that didn't match (for example `invalid class_level_id value` or `{value} is an invalid school_ids value`). Can be non-empty when `status` is `\"success\"`."}}},"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":{"/student/{student_username}/profile/":{"put":{"tags":["Student"],"parameters":[{"name":"student_username","in":"path","required":true,"description":"Student's platform username. Lowercased before lookup unless your institution signs students in with an identifier other than the username, in which case the case you send must match the record.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudentProfileCreationRequestBody"}}}},"responses":{"200":{"description":"Student created or updated. Response body is the same shape for both paths; check whether the user existed beforehand on your side if you need to distinguish. Read `errors` on every response even when `status` is `\"success\"`: values that didn't match land there as plain strings without flipping `status` to `\"failure\"`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudentProfileCreationSuccess"}}}},"400":{"description":"Validation failure. The response format varies by cause: required-field and input-type failures land as `{\"identifiers\": {...}, \"errors\": [{\"message\": \"...\"}], \"status\": \"failure\"}`, while other failures use `{\"status\": \"failure\", \"identifiers\": {...}, \"message\": \"...\"}` with a single string and no `errors` array. Parse both shapes.\n\nCommon causes:\n- Missing request body - returns `request body with student information is required.`\n- Missing required field on create or update - the message names the missing field. Required on every PUT: `first_name`, `last_name`, `email`, `term_joined`, `class_level_id`. `student_id` is also required when your institution's learning management system integration or single sign-on identifies students by student ID rather than username.\n- Student exists and is archived - returns `Student with username: {username} is archived. Cannot proceed with request.`\n- Username collides with an admin account (and your institution's configuration doesn't allow combined student/admin roles) - returns `student_username already belongs to a student admin.`\n- `term_joined` that doesn't match a term - returns `{\"status\": \"failure\", \"identifiers\": {...}, \"errors\": [...]}` with `{\"message\": \"invalid term id value for term_joined\"}` and `{\"message\": \"term_joined is a required field.\"}` in `errors`, plus a message for each other value in the body that didn't match.\n\n- `department_ids`, `school_ids`, `pre_req_waivers`, or `permission_section_ids` sent as anything other than a list, including `null` - returns `{field} must be a list` in `errors`.\n- `sis_id` sent as anything other than a string or `null` - returns `sis_id must be a string` in `errors`; a `sis_id` longer than 30 characters returns `sis_id must be at most 30 characters`.\n- `text_consent` sent as a non-null value other than a boolean or string - returns `{\"message\": \"text_consent must be a boolean or a string\"}` in the `errors` array, not as a single `message` field.\n\nOther values that don't match (`class_level_id`, `type`, `primary_campus_id`, `current_campus_id`, `primary_enrollment_level_id`, `school_ids`, `department_ids`, `pre_req_waivers`, `permission_section_ids`, `catalog_term`/`ge_catalog_term`, `expected_graduation`) don't return `400`; they land in `errors` on a `200` (for example `invalid class_level_id value` or `{value} is an invalid school_ids value`). A new student created with an unmatched `class_level_id` has no class level.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/StudentLinkResponseFailure"},{"type":"object","properties":{"errors":{"type":"array","description":"Present instead of `message` for invalid list, `sis_id`, or `text_consent` types, an overlong `sis_id`, an unmatched `term_joined`, or a missing required field on PUT.","items":{"type":"object","properties":{"message":{"type":"string"}}}}}}]}}}},"401":{"description":"Missing or invalid Bearer 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 inside the create/update transaction; nothing is saved. Response body is `{\"identifiers\": {...}, \"errors\": [{\"message\": \"Stellic couldn't process your request\"}], \"status\": \"failure\"}` (no parseable detail). Otherwise, log the request and reach out if you need to diagnose."}}}}}}
```

#### How values are matched

| Field                                                | Send                                              |
| ---------------------------------------------------- | ------------------------------------------------- |
| `class_level_id`                                     | The class level's short name                      |
| `type`                                               | The student type's name                           |
| `primary_campus_id`, `current_campus_id`             | The campus name (it's lowercased before matching) |
| `primary_enrollment_level_id`                        | The enrollment level's name                       |
| `term_joined`, `catalog_term`, `expected_graduation` | The term code from your SIS                       |
| `school_ids`, `department_ids`                       | A list of short names                             |
| `pre_req_waivers`                                    | A list of course codes                            |
| `permission_section_ids`                             | A list of section catalog IDs                     |

Values that don't match are reported in `errors` with a 200, so read `errors` on every response. An unmatched `term_joined` returns a 400 instead.

#### What happens after the call

* A new student is created with a default plan right away. They can sign in only if they're on your institution's activation list.
* Access and search are updated in a background task, and plan terms are refreshed.
* Changes to an existing student's `permission_section_ids` appear in their change history in a background task.
* When `term_joined`, `catalog_term`, `primary_campus_id`, `school_ids`, `department_ids`, `type`, or `primary_enrollment_level_id` change on an existing student, general requirements are checked in the background. Omitting `primary_campus_id` can clear the campus and trigger this check. Depending on your institution's setup and the plan type, general requirements may not be updated; ask your Stellic Representative how to refresh them. A general requirement that no longer matches can remain if it is locked or certified, or associated with an awarded diploma or a qualifying active graduation application. Contact your Stellic Representative if you need help reviewing it. The check can invalidate cached audits for plans with matching or existing general requirements, even when no requirements change.
* **Contact your Stellic Representative before triggering this check for a test student or a student without an eligible plan.** The check can change general requirements on other students' primary plans, including plans for test accounts.
* After the general-requirement check succeeds, automatically applied pathways are checked in the background, depending on your institution's setup. For this pathway check, only default plans of the primary plan type, or the empty plan type at institutions set up for prospective students, are checked; test accounts are excluded. At institutions set up for prospective students, later profile changes do not cause this pathway check to examine the primary-type default plan created by this endpoint. Contact your Stellic Representative about applying pathways to that plan.
* When `term_joined` changes the student's entry year, retained courses and placeholders keep their calendar terms; their relative study-year positions are recalculated during the call.

#### Notes

{% hint style="warning" %}
**PUT replaces the profile.** On an existing student, an omitted `primary_campus_id`, `pronouns`, `legal_name`, `student_id`, or `mobile_number` is cleared. Send the student's complete profile on every call.
{% endhint %}

* **Check the student's plan before changing `term_joined` to a different entry year.** This removes unregistered courses in past calendar terms and past-term placeholders during the call.
* **To save `mobile_number`, send `text_consent` as boolean `true` or the string `"true"` (case-insensitive).** Other boolean or string values do not grant consent and clear the number; omitting consent also clears it.
* **List fields replace the whole set.** `school_ids`, `department_ids`, and `pre_req_waivers` become exactly what you send, and an entry that doesn't match removes that link. Leave a key out to keep the current set, or use the school and department endpoints to change one link.
* **`permission_section_ids` is kept when you leave it out, even on PUT.** Unlike the fields PUT clears, an omitted list keeps the student's current permission sections. Send `[]` to remove them all. If any ID doesn't match a section, the whole list is ignored, so correct the IDs and resend the full list. Other valid profile changes are still saved on an otherwise successful call.
* Make sure permission-section catalog IDs identify sections uniquely across all terms. If an ID matches multiple sections, the endpoint selects only one without an ambiguity error. The unmatched-ID protection does not apply: replacement can remove a previously granted section sharing that ID. Resolve duplicate catalog IDs before sending `permission_section_ids`.
* **Send `sis_id` when your registration system keys students on a third identifier.** On creation, a sent `sis_id` is saved as sent, including an empty string; `null` saves an empty string. You can add a student with their username, `student_id`, and `sis_id` in one call. When you leave `sis_id` out, it's set from the username or `student_id` as before. A sent `sis_id` wins over `student_id` on updates too.
* **Check `primary_advisor_username` before sending it.** A student cannot be their own primary advisor. Send a different username. If the advisor username matches the student's username after normalization, the advisor change is ignored. An otherwise successful response returns 200 with `{username} is an invalid primary_advisor_username value: the student themselves` in `errors`. An unknown username creates an advisor account only after validation passes, inside the PUT transaction. Validation failures do not create the account; a failure inside that transaction rolls its creation back. You can't clear the advisor through this endpoint.
* **Archived students return a 400.** Restore them with [`PUT /student/{student_username}/state/`](/stellic-partner-api/student/archive-or-restore-a-student.md) first.
* **Usernames that belong to staff.** Where combined student and staff accounts aren't allowed, the call returns `student_username already belongs to a student admin.` Otherwise the staff account also becomes a student.

#### Related APIs

* [`PATCH /student/{student_username}/profile/`](/stellic-partner-api/student/partial-update-of-an-existing-student-profile.md) to change a few fields on an existing student.
* [`PUT /student/{student_username}/state/`](/stellic-partner-api/student/archive-or-restore-a-student.md) to archive or restore a student.
* [`POST /student/{student_username}/school/{school_id}`](/stellic-partner-api/student/link-a-student-to-a-school.md) and [`POST /student/{student_username}/department/{dept_id}`](/stellic-partner-api/student/link-a-student-to-a-department.md) to add one school or department.
* [`POST /planner/enrollment/program/{username}/{program_id}`](/stellic-partner-api/program-enrollments/enroll-a-student-in-a-program.md) to enroll the student in a program.
