# SharePoint REST API - Working with User Profiles

SharePoint's User Profile Service stores and surfaces rich professional data about each user — job title, department, manager, skills, and more. The `SP.UserProfiles.PeopleManager` endpoint is the primary REST surface for reading and writing this data.

---

## Introduction

SharePoint typically maintains a User Profile Service profile for Microsoft 365 users in the tenant. Profile data comes from Entra ID (for directory properties like name, title, and department), from Microsoft 365 (for profile photos, surfaced and cached by SharePoint), and from the User Profile Service itself (for SharePoint-specific and custom properties). The REST API exposes this through the `SP.UserProfiles.PeopleManager` class, which provides methods for:

- Reading profile properties for any user
- Reading your own profile
- Writing single-value and multi-value profile properties
- Following and unfollowing users
- Querying follower and following relationships

PeopleManager also exposes `SetMyProfilePicture`, but modern Microsoft 365 solutions should generally use the Microsoft 365/Graph photo APIs for profile photo management — it isn't covered in this post.

The SharePoint schema also defines `SP.UserProfiles.UserProfile` (accessible via `ProfileLoader.GetProfileLoader().GetUserProfile()`), which exposes MySite-specific properties such as `PersonalSiteCapabilities` and `PersonalSiteInstantiationState`. That entity is not covered in this post.

User profile data is not scoped to any individual SharePoint site — the PeopleManager REST resource is available at `<siteUri>/_api/SP.UserProfiles.PeopleManager` from any SharePoint site context. The examples in this post use the tenant root site; the MySite host (`{tenant}-my.sharepoint.com`) also works (validated against SharePoint Online). In a Multi-Geo tenant, user profiles have a geo location and custom profile properties should be read and updated at the user's home geo — Microsoft recommends Microsoft Graph for default-property updates in that scenario because Graph is geo-aware.

> **Authentication note:** The raw HTTP examples use a SharePoint access token. Operations whose meaning depends on a signed-in user — such as `Follow` — require delegated authentication. `GetMyProperties` can also be called with app-only authentication, but returns the application principal's pseudo-profile rather than a user's profile. App-only tokens can read any user's profile and write supported writable profile properties for other users without the ownership restriction observed with non-admin delegated authentication. For app-only authentication to SharePoint REST, Entra app-only requires certificate-based credentials — client secrets are not supported. In SPFx, use `SPHttpClient` instead of raw HTTP — it supplies the current user's authentication context and handles request digests for write operations.

> **Permission note:** The token audience for all User Profile API operations is the SharePoint Online resource (`00000003-0000-0ff1-ce00-000000000000`), the same audience as site API calls. The base scopes are `User.Read.All` (SharePoint resource) for reads and `User.ReadWrite.All` (SharePoint resource) for writes. Some operations have additional requirements; in particular, `SetMultiValuedProfileProperty` required `TermStore.ReadWrite.All` in testing. These are SharePoint-specific permission scopes, distinct from the identically named Microsoft Graph scopes.

---

## Before You Start: Headers and JSON Format

SharePoint's REST API defaults to **OData v3 in most cases** when the `OData-Version` header is not supplied. To use OData v4 — which is what SPFx's `SPHttpClient` uses by default — include the `OData-Version: 4.0` header:

```http
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none  (JSON-body requests only)
```

For a full explanation of OData versions, JSON format options, and how these headers interact, see [Understanding SharePoint REST JSON Formats](https://robwindsor.hashnode.dev/understanding-sharepoint-rest-json-formats).

---

## The PersonProperties Entity

Most PeopleManager read methods return `SP.UserProfiles.PersonProperties`, which is the standard profile response object.

| Property | Type | Description |
|---|---|---|
| `AccountName` | `string` | The claims-format login name: `i:0#.f\|membership\|{upn}`. |
| `DisplayName` | `string` | The user's display name. |
| `Title` | `string` | Job title. |
| `Email` | `string` | Email address. |
| `PictureUrl` | `string` | URL to the profile picture. As observed in SharePoint Online, includes a `?t=<timestamp>` cache-busting query parameter. |
| `PersonalUrl` | `string` | URL to the user's personal (OneDrive) site. Populated only when the user's personal site has been provisioned. |
| `PersonalSiteHostUrl` | `string` | URL to the MySite host. As observed in SharePoint Online, includes the `:443` port suffix (e.g., `https://contoso-my.sharepoint.com:443/`). Populated even before the user's personal site has been provisioned. |
| `UserUrl` | `string` | URL to the user's SharePoint profile page. |
| `LatestPost` | `string` | The user's most recent microblog post. |
| `IsFollowed` | `bool` | Whether the calling user follows this person. Reflects the signed-in user's follow list in delegated calls; not meaningful for app-only callers. |
| `DirectReports` | `Collection(string)` | Claims-format login names of direct reports. |
| `ExtendedManagers` | `Collection(string)` | Claims-format login names of the management chain above the user. |
| `ExtendedReports` | `Collection(string)` | Claims-format login names in the user's extended-report hierarchy. In SharePoint Online testing, the user's own account appeared as the first entry. |
| `Peers` | `Collection(string)` | Claims-format login names of colleagues with the same direct manager. |
| `UserProfileProperties` | `Collection(SP.KeyValue)` | Flat collection of the profile properties exposed to the caller as key-value pairs. Each entry has `Key`, `Value`, and `ValueType` fields. In SharePoint Online testing, `ValueType` was `"Edm.String"` for all returned entries, regardless of the underlying property type — type information is not preserved in this representation. Multi-value properties are returned as pipe-delimited strings (e.g., `"Golf\|Baseball\|Hiking"`). |

Profile property policy and privacy settings can affect which properties are visible to a particular caller.

> **Shape difference in collection contexts:** When `PersonProperties` objects appear inside `GetFollowersFor` or `GetPeopleFollowedBy` responses, the shape changes. `DirectReports`, `ExtendedManagers`, and `Peers` become empty strings rather than arrays. `ExtendedReports` becomes a plain string containing just the person's own login name (not an array, and not empty). `UserProfileProperties` becomes a whitespace-only string rather than a key-value collection. Use `GetPropertiesFor` to get the full shape for a specific user (validated against SharePoint Online).

`SP.KeyValue` is a simple complex type with three string properties: `Key`, `Value`, and `ValueType`.

---

## Reading Profile Data

### Get Properties for a Specific User

Returns the full `PersonProperties` for a given user, identified by their claims-format account name:

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v='i:0%23.f%7Cmembership%7Cadele@contoso.com'
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

The `accountName` parameter uses the `@v` URL alias pattern because the claims-format login name contains characters that must be percent-encoded in a URL. Percent-encode characters that require encoding (`#` → `%23`; these examples also encode `|` as `%7C`), then wrap the encoded login name in the single quotes required by the OData alias.

**Response — 200 OK** (abridged)

```json
{
  "AccountName": "i:0#.f|membership|adele@contoso.com",
  "DirectReports": [],
  "DisplayName": "Adele Vance",
  "Email": "adele@contoso.com",
  "ExtendedManagers": [
    "i:0#.f|membership|admin@contoso.com"
  ],
  "ExtendedReports": [
    "i:0#.f|membership|adele@contoso.com"
  ],
  "IsFollowed": false,
  "LatestPost": null,
  "Peers": [],
  "PersonalSiteHostUrl": "https://contoso-my.sharepoint.com:443/",
  "PersonalUrl": "https://contoso-my.sharepoint.com/personal/adele_contoso_com/",
  "PictureUrl": "https://contoso-my.sharepoint.com/User%20Photos/Profile%20Pictures/adele_contoso_com_MThumb.jpg?t=63871234567",
  "Title": "Sales Representative",
  "UserProfileProperties": [
    { "Key": "UserName", "Value": "adele@contoso.com", "ValueType": "Edm.String" },
    { "Key": "FirstName", "Value": "Adele", "ValueType": "Edm.String" },
    { "Key": "LastName", "Value": "Vance", "ValueType": "Edm.String" },
    { "Key": "WorkPhone", "Value": "+1 425 555 0100", "ValueType": "Edm.String" },
    { "Key": "Department", "Value": "Sales", "ValueType": "Edm.String" },
    { "Key": "SPS-Skills", "Value": "Sales|CRM|Negotiation", "ValueType": "Edm.String" }
  ],
  "UserUrl": "https://contoso-my.sharepoint.com/Person.aspx?accountname=i%3A0%23.f%7Cmembership%7Cadele%40contoso.com"
}
```

The `UserProfileProperties` collection shown above is abridged — in the tenant used for testing, the response contained roughly 90 entries, one per profile property defined in that tenant. Byte-array properties such as `ADGuid` and `SPS-SavedSID` serialize as the string `"System.Byte[]"` — the actual binary data is not available through this representation.

Both app-only (`User.Read.All`) and delegated (`User.Read.All`) tokens return 200 (validated against SharePoint Online).

---

### Get a Single Profile Property for a User

Returns the value of one named profile property as a plain string. There is also a `GetUserProfilePropertiesFor` method (plural) that accepts an array of property names, but it is not implemented in the SharePoint REST API — calling it returns `400` with the message `"The method GetUserProfilePropertiesFor cannot be invoked as its parameter propertiesForUser is not supported."` Use `GetPropertiesFor` and filter the `UserProfileProperties` collection client-side when you need multiple specific properties.

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetUserProfilePropertyFor(accountName=@v,propertyName='Department')?@v='i:0%23.f%7Cmembership%7Cadele@contoso.com'
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

**Response — 200 OK**

```json
{
  "value": "Sales"
}
```

The property name is the internal profile property name — the same `Key` value that appears in the `UserProfileProperties` collection. Both app-only and delegated tokens return 200 (validated against SharePoint Online).

---

### Get Your Own Properties

Returns the `PersonProperties` for the caller. With delegated authentication, this returns the signed-in user's real profile. With app-only authentication, it returns the application service principal's pseudo-profile — `AccountName` is `"i:0i.t|00000003-0000-0ff1-ce00-000000000000|app@sharepoint"`, `Email` is null, and the remaining fields reflect the app identity rather than any user. `GetMyProperties` with an app-only token does not error (returns 200), but the data is not meaningful for user profile work (validated against SharePoint Online):

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetMyProperties
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

> Minimum scope: `User.Read.All` (SharePoint resource). Both app-only and delegated tokens return 200. With app-only, the response reflects the app service principal rather than any user — not useful for profile data.

**Response — 200 OK** — same shape as `GetPropertiesFor` (validated against SharePoint Online).

---

## Writing Profile Data

Profile write operations are performed through `SetSingleValueProfileProperty` and `SetMultiValuedProfileProperty`. Both are `POST` actions on the `PeopleManager` endpoint.

> **Documentation discrepancy:** Microsoft's current general user-profile programming documentation states that changing profile properties through REST/client APIs is not implemented and that client profiles are read-only except for the profile picture. However, SharePoint Online accepted both `SetSingleValueProfileProperty` and `SetMultiValuedProfileProperty` through REST in live testing. Microsoft's more recent troubleshooting guidance also explicitly documents Entra app-only user-profile updates using `User.ReadWrite.All`.

Before writing profile properties, consider the source of the data. Properties synchronized from Entra ID — such as display name, job title, and department — are periodically overwritten by the User Profile Service sync job; updating them in SharePoint will not persist, and those properties should be changed at their source in Entra ID. SharePoint-specific and custom profile properties are the appropriate targets for these write operations. Note that the synchronization is one-directional: custom SharePoint profile properties do not sync back to Entra ID.

> **Delegated write restriction:** With delegated authentication, an ordinary user can update their own profile. In testing, attempting to update another user's profile with a non-admin delegated user returned `401` with the message `"This operation requires you to be managing your own data or have administrator privileges."` Delegated administrator behavior was not tested. App-only authentication successfully updated other users' profiles (both validated against SharePoint Online).

### Set a Single-Value Profile Property

Updates the value of one profile property for a user:

```http
POST https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/SetSingleValueProfileProperty
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "accountName": "i:0#.f|membership|adele@contoso.com",
  "propertyName": "AboutMe",
  "propertyValue": "Sales professional based in Seattle."
}
```

> Minimum scope: `User.ReadWrite.All` (SharePoint resource). Both app-only and delegated tokens work at this scope. With the non-admin delegated user tested, `accountName` had to match the signed-in user. Delegated administrator cross-user behavior was not tested (validated against SharePoint Online).

**Response — 204 No Content**

---

### Set a Multi-Value Profile Property

Updates the value of a multi-value profile property. The values are provided as a JSON string array:

```http
POST https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/SetMultiValuedProfileProperty
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "accountName": "i:0#.f|membership|adele@contoso.com",
  "propertyName": "SPS-Skills",
  "propertyValues": ["Sales", "CRM", "Negotiation"]
}
```

> In SharePoint Online testing, `SetMultiValuedProfileProperty` required both `User.ReadWrite.All` **and** `TermStore.ReadWrite.All` on the SharePoint resource — a token with `User.ReadWrite.All` alone returned `401` regardless of token type, the caller's SharePoint permission level, or whether the caller is a tenant global admin. Microsoft's troubleshooting documentation associates the `TermStore.ReadWrite.All` requirement specifically with taxonomy-backed properties, but live testing found that the REST method required it even for a plain-text multi-value property with no taxonomy backing (validated against SharePoint Online). That discrepancy is worth noting if you encounter an unexpected `401` in this scenario.

**Response — 204 No Content**

When read back through `GetPropertiesFor`, multi-value properties appear in `UserProfileProperties` as a pipe-delimited string (e.g., `"Sales|CRM|Negotiation"`).

---

## Following Users

> **Deprecation notice:** Following People is a deprecated classic SharePoint social capability. The endpoints documented in this section remain callable in SharePoint Online, as confirmed by live testing, but this is a legacy system — these APIs are not recommended as the basis for new modern SharePoint solutions.

PeopleManager exposes follow/unfollow operations and follower/following queries.

### Follow a User

```http
POST https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/Follow
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <delegated token>

{
  "accountName": "i:0#.f|membership|adele@contoso.com"
}
```

> Follow is inherently a delegated operation — it represents a signed-in user choosing to follow another. App-only tokens return `500` with the message `"Sorry, you don't have a license to use Newsfeed. Please contact your help desk."` (validated against SharePoint Online). Minimum delegated scope: `User.ReadWrite.All` (SharePoint resource).

**Response — 204 No Content**

### Stop Following a User

```http
POST https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/StopFollowing
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <delegated token>

{
  "accountName": "i:0#.f|membership|adele@contoso.com"
}
```

> Same permission requirements as `Follow`: delegated only, minimum scope `User.ReadWrite.All` (SharePoint resource). App-only returned 500 in testing with the message `"You are not following this person."` — the app principal follows no one, so there is nothing to unfollow. This differs from `Follow` app-only, which returns a Newsfeed license error before the operation is attempted (validated against SharePoint Online).

**Response — 204 No Content** (validated against SharePoint Online).

---

### Check If You Are Followed By a User

Returns whether a specific user follows the caller:

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/AmIFollowedBy(accountName=@v)?@v='i:0%23.f%7Cmembership%7Cadele@contoso.com'
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <delegated token>
```

> Minimum scope: `User.Read.All` (SharePoint resource). "I" refers to the signed-in user. App-only tokens return 200 with `false` — the application service principal is not followed by anyone (validated against SharePoint Online).

**Response — 200 OK** (validated against SharePoint Online)

```json
{
  "value": true
}
```

---

### Check If You Are Following a User

Returns whether the caller follows a specific user:

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/AmIFollowing(accountName=@v)?@v='i:0%23.f%7Cmembership%7Cadele@contoso.com'
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <delegated token>
```

> Minimum scope: `User.Read.All` (SharePoint resource). "I" refers to the signed-in user in delegated calls. App-only tokens return 200 with `false` — the application service principal follows no one (validated against SharePoint Online).

**Response — 200 OK** (validated against SharePoint Online)

```json
{
  "value": true
}
```

---

### Get Followers for a User

Returns the list of users who follow a given account:

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetFollowersFor(accountName=@v)?@v='i:0%23.f%7Cmembership%7Cadele@contoso.com'
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

**Response — 200 OK** (abridged)

```json
{
  "value": [
    {
      "AccountName": "i:0#.f|membership|alex@contoso.com",
      "DisplayName": "Alex Wilber",
      "Email": "alex@contoso.com",
      "DirectReports": "",
      "ExtendedManagers": "",
      "ExtendedReports": "i:0#.f|membership|alex@contoso.com",
      "Peers": "",
      "UserProfileProperties": " ",
      "UserUrl": "https://contoso-my.sharepoint.com/Person.aspx?accountname=..."
    }
  ]
}
```

Note the truncated shape: `DirectReports`, `ExtendedManagers`, and `Peers` are empty strings (not arrays); `ExtendedReports` is a plain string containing just the follower's own login name (not an array); and `UserProfileProperties` is a whitespace-only string (not a key-value collection). This is the collection context shape difference described in the PersonProperties section. Use `GetPropertiesFor` if you need the full profile data for each follower.

Both app-only and delegated tokens return 200 (validated against SharePoint Online).

---

### Get Users Followed By a User

Returns the list of users that a given account follows:

```http
GET https://contoso.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetPeopleFollowedBy(accountName=@v)?@v='i:0%23.f%7Cmembership%7Cadele@contoso.com'
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

**Response — 200 OK** — same shape as `GetFollowersFor` (validated against SharePoint Online).

---

## Quick Reference

### Headers

| Header | Notes |
|---|---|
| `Authorization: Bearer <token>` | Required for the raw HTTP OAuth examples in this post. In SPFx, `SPHttpClient` handles authentication automatically. |
| `Accept: application/json;odata.metadata=none` | Required for a JSON response. Omitting it causes SharePoint to return Atom XML. |
| `Content-Type: application/json;odata.metadata=none` | Required on requests with a JSON body (POST write operations). |
| `OData-Version: 4.0` | Activates OData v4 behavior. Used throughout the examples in this post. |

### Response Status Codes

| Operation | Status | Body |
|---|---|---|
| GetPropertiesFor | 200 OK | `PersonProperties` entity |
| GetUserProfilePropertyFor | 200 OK | `{ "value": "..." }` scalar |
| GetMyProperties | 200 OK | `PersonProperties` entity |
| GetFollowersFor / GetPeopleFollowedBy | 200 OK | `{ "value": [...] }` collection |
| AmIFollowedBy / AmIFollowing | 200 OK | `{ "value": true/false }` scalar |
| SetSingleValueProfileProperty | 204 No Content | Empty |
| SetMultiValuedProfileProperty | 204 No Content | Empty |
| Follow / StopFollowing | 204 No Content | Empty |
| Follow (app-only) | 500 | `"Sorry, you don't have a license to use Newsfeed. Please contact your help desk."` |
| StopFollowing (app-only) | 500 | `"You are not following this person."` |
| SetMultiValuedProfileProperty without TermStore.ReadWrite.All | 401 | `"The current user has insufficient permissions to perform this operation."` |
| Cross-user write with non-admin delegated token | 401 | `"This operation requires you to be managing your own data..."` |

### Permission Requirements

All User Profile API calls use the **SharePoint Online resource** (`00000003-0000-0ff1-ce00-000000000000`) — the same resource as site-level API calls. The scopes below are SharePoint-specific and are distinct from identically named Microsoft Graph scopes.

| Operation | Minimum delegated scope | Notes | Minimum app-only scope |
|---|---|---|---|
| GetPropertiesFor | `User.Read.All` (SharePoint) | Any user's profile | `User.Read.All` (SharePoint) |
| GetUserProfilePropertyFor | `User.Read.All` (SharePoint) | Any user's profile property | `User.Read.All` (SharePoint) |
| GetMyProperties | `User.Read.All` (SharePoint) | Returns caller's profile | `User.Read.All` (SharePoint) — returns app pseudo-profile, not useful |
| GetFollowersFor / GetPeopleFollowedBy | `User.Read.All` (SharePoint) | — | `User.Read.All` (SharePoint) |
| AmIFollowedBy | `User.Read.All` (SharePoint) | "I" = signed-in user | `User.Read.All` (SharePoint) — app-only returns 200/false (app principal is followed by no one) |
| AmIFollowing | `User.Read.All` (SharePoint) | "I" = signed-in user | `User.Read.All` (SharePoint) — app-only returns 200/false (app principal follows no one) |
| SetSingleValueProfileProperty | `User.ReadWrite.All` (SharePoint) | Non-admin: own profile only in testing; delegated administrator behavior not tested | `User.ReadWrite.All` (SharePoint) — any user |
| SetMultiValuedProfileProperty | `User.ReadWrite.All` + `TermStore.ReadWrite.All` (SharePoint) | Non-admin: own profile only in testing; delegated administrator behavior not tested; `TermStore.ReadWrite.All` required in testing even for non-taxonomy properties | `User.ReadWrite.All` + `TermStore.ReadWrite.All` (SharePoint) — any user |
| Follow / StopFollowing | `User.ReadWrite.All` (SharePoint) | Delegated only | Not applicable — app-only returns 500 (`Follow`: Newsfeed license error; `StopFollowing`: "You are not following this person") |

> **Note:** All scopes listed are on the **Office 365 SharePoint Online** API (resource `00000003-0000-0ff1-ce00-000000000000`), not Microsoft Graph. An SPFx solution using `SPHttpClient` does not require explicit scope grants — it calls SharePoint as the current user, whose profile access is governed by SharePoint's own rules.

---

## Wrapping Up

`SP.UserProfiles.PeopleManager` provides a unified endpoint for reading and writing SharePoint user profile data. A few things worth keeping in mind:

- Profile operations are not scoped to the SharePoint site used to make the request. The resource is available as `<siteUri>/_api/SP.UserProfiles.PeopleManager`; the tenant root and MySite host both worked in SharePoint Online testing.
- The `accountName` parameter accepts the claims-format login name (`i:0#.f|membership|user@contoso.com`). When used with the `@v` alias pattern, percent-encode characters that require encoding (`#` → `%23`; these examples also encode `|` as `%7C`), then wrap the encoded login name in the single quotes required by the OData alias.
- `UserProfileProperties` in the `PersonProperties` response is a flat collection of all profile properties as key-value pairs. All `ValueType` entries read `"Edm.String"` regardless of the actual property type, and byte-array properties like `ADGuid` serialize as the string `"System.Byte[]"`. Multi-value properties appear as pipe-delimited strings.
- The `PersonProperties` shape changes when entries appear inside follower/following collection responses — `DirectReports`, `ExtendedManagers`, and `Peers` become empty strings; `ExtendedReports` becomes a plain string of just the person's own login name; and `UserProfileProperties` becomes a whitespace string. Use `GetPropertiesFor` when you need the full shape for a specific user.
- `SetMultiValuedProfileProperty` requires `TermStore.ReadWrite.All` in addition to `User.ReadWrite.All`. In SharePoint Online testing, this was required even for a plain-text multi-value property with no taxonomy backing, despite Microsoft's documentation associating that permission with taxonomy updates.
- `Follow`, `StopFollowing`, `AmIFollowedBy`, `AmIFollowing`, `GetFollowersFor`, and `GetPeopleFollowedBy` are part of the classic SharePoint Following People feature, which is deprecated in SharePoint Online. The endpoints remain callable, but these APIs are not recommended for new modern solutions.
- `Follow` and `StopFollowing` are inherently delegated operations. App-only calls return 500 in both cases, but with different errors: `Follow` returns a Newsfeed license error, while `StopFollowing` returns `"You are not following this person."` — consistent with the app principal having no follow relationship (both validated against SharePoint Online).
- With delegated authentication, an ordinary user can update their own profile. In testing, a non-admin delegated user attempting to update another user's profile returned 401. Delegated administrator behavior was not tested. App-only authentication successfully updated other users' profiles.

Happy coding!

