Skip to main content

Command Palette

Search for a command to run...

SharePoint REST API - Working with Managed Metadata

Updated
31 min readView as Markdown
SharePoint REST API - Working with Managed Metadata
R

I'm a .NET/M365 developer, trainer, author, MVP & MCT Alumni

SharePoint's Managed Metadata service provides a centralized taxonomy — organized into term groups, term sets, and terms — that can be applied as column values across lists and libraries. This post covers two distinct REST surfaces: the /_api/v2.1/termstore endpoint for reading and writing term store structure, and the classic /_api/ endpoint for creating and using managed metadata columns in lists. It follows on from the earlier posts in this series on lists, list items, fields, views, content types, files, folders, sites, user profiles, search, and batch.


Introduction

Managed Metadata in SharePoint has two layers that each require their own REST surface:

  • Term store structure — the hierarchy of groups, sets, and terms that defines your taxonomy. Managed via /_api/v2.1/termstore, which is a Graph-aligned REST API introduced as the modern replacement for the legacy taxonomy endpoints. The v2.1 resource model closely resembles Microsoft Graph's termStore model, but the SharePoint-hosted surface exposes some additional properties, so the Graph API reference should not be assumed to document every v2.1 detail.
  • Managed metadata columns — the list and library columns that reference a term set and store term values on list items. Managed via the classic /_api/web/lists(guid'...')/fields surface.
/_api/v2.1/termstore              (term store structure — groups, sets, terms)
/_api/web/lists(guid'...')/fields (managed metadata columns on lists)
/_api/web/fields                  (managed metadata site columns)

In the single-geo SharePoint Online tenant used for testing, the term store root entity returned by GET /_api/v2.1/termstore was identical from the root site URL or any site collection URL. Managed Metadata is Multi-Geo-aware, however: in Multi-Geo tenants, taxonomy can differ by geography — metadata created in the default geo is replicated to satellite geos, while metadata created in a satellite geo is available only in that geo. The site URL does affect the groups collection endpoint (/termstore/groups): a site-scoped URL surfaces that site's own local term group (if one has been initialized), while the root URL returns global groups and any site-local groups created through the v2.1 REST API. Both URL contexts return global and system groups.

Authentication note: This post covers two separate permission surfaces. Term store structure operations (/_api/v2.1/termstore) require a token with TermStore.Read.All (read) or TermStore.ReadWrite.All (write) — these are SharePoint Online resource scopes (00000003-0000-0ff1-ce00-000000000000), distinct from the same-named Microsoft Graph scopes. Managed metadata column operations (/_api/web/lists/.../fields) require AllSites.Manage / Sites.Manage.All for field creation and deletion, and AllSites.Write / Sites.ReadWrite.All for writing term values to list items — these are the same site API scopes used throughout the rest of this series. For Entra app-only access to SharePoint REST, Microsoft documents certificate-based authentication; client-secret app-only tokens are not accepted. The legacy SharePoint ACS app-only model was retired on November 27, 2023 and stopped working on April 2, 2026. In SPFx, use SPHttpClient instead — it supplies the current user's SharePoint authentication context.

SharePoint v2.1 vs. Microsoft Graph: Although the /_api/v2.1/termstore surface is Graph-aligned, its authentication behavior should not be assumed to be identical to calling Microsoft Graph term-store endpoints directly. App-only tokens with SharePoint TermStore.Read.All / TermStore.ReadWrite.All succeeded against /_api/v2.1/termstore in testing. Microsoft's individual Graph term-store operation pages currently list application authentication as unsupported for many operations, although Microsoft's central Graph permissions reference defines application TermStore.Read.All and TermStore.ReadWrite.All permissions — Microsoft's own Graph documentation is currently inconsistent on this point. This post does not test app-only calls to graph.microsoft.com; the app-only behavior documented here applies specifically to the SharePoint-hosted /_api/v2.1/termstore surface.


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 on your requests.

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.


Part 1: Term Store Structure

The Term Store Hierarchy

The term store is organized as a three-level hierarchy:

Term store
└── Groups
    └── Sets
        └── Terms (optionally nested)

A group is a top-level organizational container. Groups can be regular tenant-level groups, system groups managed by SharePoint for internal purposes, or site-local groups. A set (also called a term set) belongs to exactly one group and defines the named collection of terms available for tagging. Terms live inside a set and can be nested — a term can have child terms, creating a hierarchy within the set.

The Term Store Root

GET https://contoso.sharepoint.com/_api/v2.1/termstore
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK

{
  "id": "a7a0c1dc-0001-0001-0001-000000000001",
  "name": "Taxonomy_a7a0c1dc-0001-0001-0001-000000000001",
  "defaultLanguageTag": "en-US",
  "languageTags": [ "en-US" ]
}

The id here is the term store ID — you will need it when binding a managed metadata column to a term set (the SspId property on the field). Both app-only and delegated tokens with TermStore.Read.All return identical results. In the single-geo SharePoint Online tenant used for testing, site-scoped URLs (e.g. /sites/marketing/_api/v2.1/termstore) returned the same store root entity as the root URL.

Legacy endpoints: /_api/v1.0/termstore and /_api/SP.Taxonomy.Internal.TaxonomyService returned 404 in SharePoint Online testing. The examples in this post use the /_api/v2.1/termstore surface.

Groups

List all groups

GET https://contoso.sharepoint.com/_api/v2.1/termstore/groups
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK (abridged)

{
  "value": [
    {
      "id": "b2b3c4d5-0001-0001-0001-000000000001",
      "displayName": "Marketing",
      "name": "Marketing",
      "description": "",
      "createdDateTime": "2025-01-15T14:22:00Z",
      "lastModifiedDateTime": "2026-03-10T09:00:00Z",
      "type": "RegularGroup",
      "scope": "global"
    }
  ]
}

Group properties:

Property Type Description
id string (GUID) Unique identifier of the group.
displayName string Display name of the group.
name string Same as displayName.
description string Optional description.
createdDateTime string ISO 8601 creation timestamp.
lastModifiedDateTime string ISO 8601 last-modified timestamp.
type string RegularGroup for user-created groups; SystemGroup for groups managed by SharePoint; SiteCollectionGroup for site-local groups.
scope string global for regular groups; system for system groups; siteCollection for site-local groups.

Groups collection and site context (validated against SharePoint Online): The groups collection endpoint returns different results depending on the calling URL. A site-scoped URL (e.g. /sites/marketing/_api/v2.1/termstore/groups) returns global and system groups plus that site's own native site-local group, if one has been initialized. The root URL (/_api/v2.1/termstore/groups) returns global and system groups plus site-local groups created through the v2.1 REST API — it does not surface native site-local groups created through the Term Store Management tool. Any group can be fetched by direct ID from any URL context regardless of which collection view it appears in.

Get a single group

GET https://contoso.sharepoint.com/_api/v2.1/termstore/groups/b2b3c4d5-0001-0001-0001-000000000001
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK — the same group object shape.

Accessing sets directly without a group: GET termstore/sets (without a group path segment) returns 400 apiNotFound. Sets must be accessed through their parent group (groups/{id}/sets) or by direct ID (sets/{id}).

Sets

List sets in a group

GET https://contoso.sharepoint.com/_api/v2.1/termstore/groups/b2b3c4d5-0001-0001-0001-000000000001/sets
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK (abridged)

{
  "value": [
    {
      "id": "c3c4d5e6-0002-0002-0002-000000000002",
      "groupId": "b2b3c4d5-0001-0001-0001-000000000001",
      "localizedNames": [
        { "name": "Departments", "languageTag": "en-US" }
      ],
      "description": "Department taxonomy",
      "childrenCount": 4,
      "isOpen": false,
      "createdDateTime": "2025-01-15T14:25:00Z"
    }
  ]
}

Set properties:

Property Type Description
id string (GUID) Unique identifier of the set. This is the TermSetId used when binding a column.
groupId string (GUID) ID of the parent group.
localizedNames array Array of {name, languageTag} objects — the set's display name in each configured language.
description string Optional description.
childrenCount int Number of root-level terms in the set.
isOpen bool When true, users without taxonomy contributor rights can submit new terms through tagging experiences when the managed metadata column permits fill-in choices. In SharePoint Online testing, a delegated caller with TermStore.ReadWrite.All but no taxonomy role could also POST a new term directly to an open set's /children endpoint. When false, delegated users without the appropriate taxonomy-management role cannot add terms. App-only tokens behave differently because they are not subject to signed-in-user taxonomy roles.
createdDateTime string ISO 8601 creation timestamp.

Get a set by ID

You can also access a set directly without knowing its group:

GET https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK — the same set object shape, including groupId.

Terms

Get all terms in a set (flat)

GET https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Returns all terms at all hierarchy levels in a flat array — parent and child terms appear at the same level in the response.

Get root-level terms only

GET https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/children
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Returns only the top-level terms — terms that have no parent term.

Get a single term

GET https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms/d4d5e6f7-0003-0003-0003-000000000003
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK

{
  "id": "d4d5e6f7-0003-0003-0003-000000000003",
  "isDeprecated": false,
  "childrenCount": 2,
  "createdDateTime": "2025-02-01T10:00:00Z",
  "lastModifiedDateTime": "2026-01-20T08:30:00Z",
  "labels": [
    { "name": "Engineering", "isDefault": true, "languageTag": "en-US" }
  ],
  "descriptions": [],
  "isAvailableForTagging": [
    { "setId": "c3c4d5e6-0002-0002-0002-000000000002", "isAvailable": true }
  ]
}

Term properties:

Property Type Description
id string (GUID) Unique identifier of the term. This is the TermGuid used when writing a term value to a list item.
isDeprecated bool Whether the term has been marked deprecated. Deprecated terms are unavailable for new tagging through SharePoint's normal tagging UI; existing assignments remain. Writing a deprecated term's GUID explicitly through the REST API succeeds — SharePoint does not enforce the restriction at the API level (validated against SharePoint Online).
childrenCount int Number of direct child terms.
createdDateTime string ISO 8601 creation timestamp.
lastModifiedDateTime string ISO 8601 last-modified timestamp.
labels array Array of {name, isDefault, languageTag} objects. Exactly one label per language should have isDefault: true.
descriptions array Array of {description, languageTag} objects. May be empty.
isAvailableForTagging array Array of {setId, isAvailable} objects indicating whether this term can be selected in each set it belongs to.

Get children of a term

GET https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms/d4d5e6f7-0003-0003-0003-000000000003/children
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK — array of child term objects. Returns an empty value array when the term has no children.

Get relations for a term

GET https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms/d4d5e6f7-0003-0003-0003-000000000003/relations
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 200 OK — array of relation objects. Returns an empty value array for terms with no explicit relations.

OData Query Options

In SharePoint Online testing, the /_api/v2.1/termstore endpoints supported the following subset of OData query options:

Option Behavior
$select Worked on all resource and collection endpoints tested — response is limited to the specified properties.
$filter Worked on collections (e.g. groups?$filter=name eq 'Marketing').
$top Worked. When results are truncated, the response includes an @odata.nextLink with a $skiptoken for paging.
$skiptoken Use the full @odata.nextLink URL to follow pages — do not construct $skiptoken manually.
$orderby Worked for simple scalar properties (e.g. terms?$orderby=createdDateTime desc). Lambda expressions (any/all) return 400.
$expand Silently ignored (validated against SharePoint Online). Requests that include $expand return 200 with the base entity only — no expanded data is included in the response.

Creating a Group

POST https://contoso.sharepoint.com/_api/v2.1/termstore/groups
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "displayName": "Marketing",
  "description": "Marketing taxonomy"
}

Response — 201 Created with the full group entity, including the generated id.

displayName is required. description is optional. A duplicate displayName returns 409 with error code nameAlreadyExists and message "Group names must be unique."

System group protection: Attempting to modify or delete a system group (those with type: "SystemGroup") returns 403. Creating a set inside a system group also returns 403.

Creating a Set

Sets must be created through their parent group — a POST directly to termstore/sets returns 400:

POST https://contoso.sharepoint.com/_api/v2.1/termstore/groups/b2b3c4d5-0001-0001-0001-000000000001/sets
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "localizedNames": [
    { "name": "Departments", "languageTag": "en-US" }
  ],
  "description": "Department taxonomy",
  "isOpen": false
}

Response — 201 Created with the full set entity, including the generated id (this id is the TermSetId you will need when binding a column).

localizedNames is required and must contain at least one entry. description and isOpen are optional.

Creating Terms

Create a root-level term

Use the set's /children endpoint to create a term at the root of the set:

POST https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/children
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "labels": [
    { "name": "Engineering", "isDefault": true, "languageTag": "en-US" }
  ],
  "descriptions": [
    { "description": "Engineering division", "languageTag": "en-US" }
  ]
}

Response — 201 Created with the full term entity, including the generated id (this id is the TermGuid you use when writing a term value to a list item).

Create a child term

To create a term nested under an existing term, POST to that parent term's /children endpoint:

POST https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms/d4d5e6f7-0003-0003-0003-000000000003/children
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "labels": [
    { "name": "Platform Engineering", "isDefault": true, "languageTag": "en-US" }
  ]
}

Response — 201 Created — the same term entity shape as root term creation.

A duplicate default label within the same parent returns 409 with error code nameAlreadyExists.

Updating

PATCH works at all levels — root, group, set, and term — and returns 200 OK with the full updated entity:

Update the term-store root

PATCH https://contoso.sharepoint.com/_api/v2.1/termstore
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "defaultLanguageTag": "en-US"
}

Response — 200 OK with the updated term-store root entity.

Update a group

PATCH https://contoso.sharepoint.com/_api/v2.1/termstore/groups/b2b3c4d5-0001-0001-0001-000000000001
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "displayName": "Marketing & Comms",
  "description": "Updated description"
}

Response — 200 OK with the updated group entity.

Update a set

PATCH https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "localizedNames": [
    { "name": "Departments Updated", "languageTag": "en-US" }
  ],
  "isOpen": true
}

Response — 200 OK with the updated set entity.

Update a term

PATCH https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms/d4d5e6f7-0003-0003-0003-000000000003
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json
Authorization: Bearer <token>

{
  "labels": [
    { "name": "Engineering Dept", "isDefault": true, "languageTag": "en-US" }
  ]
}

Response — 200 OK with the updated term entity.

Labels replacement (validated against SharePoint Online): PATCH on a term's labels property replaces the entire labels array. If the term had labels in multiple languages and you send only one, the others are removed. Always include all labels you want to retain in the PATCH body.

Deleting

Delete a term

DELETE https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002/terms/d4d5e6f7-0003-0003-0003-000000000003
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 204 No Content

Deleting a term cascade-deletes all of its child terms (validated against SharePoint Online). After deletion, GET requests for the deleted term or any of its former children return 404.

Delete a set

DELETE https://contoso.sharepoint.com/_api/v2.1/termstore/sets/c3c4d5e6-0002-0002-0002-000000000002
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 204 No Content

Deleting a set cascade-deletes all terms in that set, regardless of how deeply nested they are (validated against SharePoint Online).

Delete a group

Groups do not cascade-delete their sets. The group must be empty (all sets deleted first) before it can be deleted:

DELETE https://contoso.sharepoint.com/_api/v2.1/termstore/groups/b2b3c4d5-0001-0001-0001-000000000001
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Response — 204 No Content (if group is empty)

Response — 403 Forbidden (if group still contains sets)

{
  "error": {
    "code": "notAllowed",
    "message": "A Group cannot be deleted unless it is empty."
  }
}

Delete all sets in the group first, then delete the group.


Part 2: Managed Metadata Columns

Managed metadata columns connect list items to terms in the term store. SharePoint represents them as TaxonomyFieldType (single value) or TaxonomyFieldTypeMulti (multiple values). These field types behave differently from other column types in several important ways that this section covers in detail.

Site-local term sets: SharePoint supports term sets that are local to a specific site and visible only within that site. This post covers only global term sets rather than site-local term sets. In SharePoint Online testing, creating term sets within a site-local group returned 403 Forbidden for an app-only token with TermStore.ReadWrite.All — the reason for the 403, including whether another permission or a different access pattern is required, was not determined. Site-local term set management is not covered here.

Creating a Managed Metadata Column

Creating a managed metadata column is a two-step process: first create the unbound field, then bind it to a term set.

Step 1: Create the Field

In SharePoint Online testing, posting a managed metadata field directly to the fields collection returned 500 with message "One or more field types are not installed properly." Use createFieldAsXml instead:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/fields/createFieldAsXml
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "parameters": {
    "SchemaXml": "<Field Type='TaxonomyFieldType' DisplayName='Category' Name='Category' StaticName='Category' ShowField='Term1033' />",
    "Options": 8
  }
}

Response — 200 OK (note: 200, not 201) with the full field entity.

The newly created field will have SspId and TermSetId set to zero GUIDs — it is unbound and cannot accept term values until Step 2. Options: 8 is AddFieldInternalNameHint, which tells SharePoint to treat the Name value in the SchemaXml as the internal-name hint rather than generating one from the display name.

ShowField='Term1033' configures the taxonomy picker to resolve term labels using the English locale (LCID 1033). In the English-language SharePoint Online environment used for testing, SharePoint injected ShowField='Term1033' when the attribute was omitted from the SchemaXml. For multilingual or non-English environments, specify the appropriate label behavior explicitly rather than relying on this observed default.

Creating a multi-value column: Add Mult='TRUE' to the SchemaXml attribute list:

<Field Type='TaxonomyFieldTypeMulti' DisplayName='Tags' Name='Tags' StaticName='Tags' Mult='TRUE' ShowField='Term1033' />

Permission note: createFieldAsXml requires AllSites.Manage for delegated access or Sites.Manage.All for app-only. AllSites.Write / Sites.ReadWrite.All is not sufficient — those scopes return 403 Forbidden (validated against SharePoint Online).

The Companion Note Field

Every managed metadata column — both TaxonomyFieldType and TaxonomyFieldTypeMulti — automatically gets a paired hidden Note field when the taxonomy field is created. In SharePoint Online testing, the automatically created companion field had the following characteristics:

  • Has Hidden: true — it does not appear in list views or forms
  • Has a Title of {FieldDisplayName}_0 (e.g. Category_0)
  • Has an InternalName that matches the companion field's own GUID with dashes removed (and n prefixed if the first character is a digit)
  • Is automatically deleted when the taxonomy field is deleted — you do not manage it separately

The companion field is particularly useful for reading the human-readable label of a single-value taxonomy field and, for direct list-item POST/MERGE updates, for writing multi-value term values (see the Writing Values sections below).

To use the companion field in writes, start by retrieving its GUID via the taxonomy field's TextField property:

GET https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/fields(guid'f1e2d3c4-0005-0005-0005-000000000005')?$select=TextField
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

The TextField response contains the companion field's GUID. Use that GUID to retrieve the companion field's EntityPropertyName — the property name SharePoint uses for the field in list-item entities:

GET https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/fields(guid'{companionFieldGuid}')?$select=InternalName,EntityPropertyName
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>

Use the EntityPropertyName value as the JSON property key in item writes. Do not derive this from the taxonomy field's own GUID — retrieve it from the companion field directly. In SharePoint Online testing, the companion field's EntityPropertyName and InternalName matched the pattern described above (GUID with dashes removed, n prefix when needed), but reading the property avoids depending on that naming convention.

Step 2: Bind to a Term Set

After the field is created, PATCH it to bind it to the term set. You need the term store ID (from GET /_api/v2.1/termstore) and the term set ID (from GET /_api/v2.1/termstore/groups/{id}/sets or sets/{id}):

PATCH https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/fields(guid'f1e2d3c4-0005-0005-0005-000000000005')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "SspId": "a7a0c1dc-0001-0001-0001-000000000001",
  "TermSetId": "c3c4d5e6-0002-0002-0002-000000000002",
  "AnchorId": "00000000-0000-0000-0000-000000000000"
}

Response — 204 No Content

SspId is the term store ID. TermSetId is the set to bind to. AnchorId is the GUID of a term to use as the root of the picker — an all-zeros GUID means no restriction (the entire set is available for selection). After this PATCH, a subsequent GET of the field shows IsTermSetValid: true.

Site Columns

To create a site-level managed metadata column (reusable across lists), use the web-level fields endpoint instead of the list-level one:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/fields/createFieldAsXml

The SchemaXml and Options are identical to the list-column creation above. The response shows Scope set to the site's server-relative URL rather than the list's URL.

To add an existing site column to a list, POST createFieldAsXml to the list's fields endpoint with the site column's SchemaXml (which includes an ID attribute carrying the original field GUID):

  1. Read the site column's SchemaXml: GET /_api/web/fields(guid'{fieldId}')?$select=SchemaXml
  2. POST that SchemaXml to the list fields endpoint
POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/fields/createFieldAsXml
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "parameters": {
    "SchemaXml": "<Field Type='TaxonomyFieldType' ID='{f1e2d3c4-0005-0005-0005-000000000005}' DisplayName='Category' Name='Category' StaticName='Category' ShowField='Term1033' SspId='a7a0c1dc-0001-0001-0001-000000000001' TermSetId='c3c4d5e6-0002-0002-0002-000000000002' />",
    "Options": 8
  }
}

The SchemaXml above is a simplified representation — the SchemaXml returned by GET on a real site column will contain additional attributes that SharePoint adds at creation time. Use the GET response value verbatim rather than constructing it by hand.

Response — 200 OK — the returned field has the same Id as the original site column. The term set binding from the SchemaXml is inherited — no separate PATCH is needed. The companion Note field that was created alongside the site column is automatically available on the list as well.

Site column delete lifecycle: Deleting the field instance from the list (DELETE list/fields/{id}) removes it from the list but leaves the original site column intact at the web level. Deleting the site column itself (DELETE web/fields/{id}) removes it from the web; subsequent GET requests for that field return 400 Bad Request with message "Invalid field name" (not 404, which is the typical "not found" pattern elsewhere in the API).

Writing Single-Value Term Values (TaxonomyFieldType)

There are two approaches for setting a TaxonomyFieldType value on a list item. Both produce identical stored values; use whichever fits your data model.

Approach 1: Structured value object

Use the @odata.type annotation to provide the SP.Taxonomy.TaxonomyFieldValue complex type:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/items(7)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>
X-HTTP-Method: MERGE
If-Match: *

{
  "Category": {
    "@odata.type": "#SP.Taxonomy.TaxonomyFieldValue",
    "Label": "Engineering",
    "TermGuid": "d4d5e6f7-0003-0003-0003-000000000003",
    "WssId": -1
  }
}

TermGuid is the term's id from the term store. Label should be the term's default label. WssId: -1 tells SharePoint to assign the actual WssId — -1 is the normal value to supply when you want SharePoint to resolve the WssId. An invalid TermGuid returns 400 with message "The given guid does not exist in the term store."

Deprecated terms: Writing a deprecated term's GUID via REST succeeds without error — SharePoint does not enforce the deprecated state at the API level. Deprecated terms can be used in item writes just like active terms (validated against SharePoint Online).

Do not mix OData v3 payload syntax with OData v4: When OData-Version: 4.0 is used (as in all examples in this post), supplying the v3 __metadata type wrapper ("__metadata": {"type": "SP.Taxonomy.TaxonomyFieldValue"}) returns 400 Bad Request. Use the @odata.type annotation shown above, which is correct for OData v4 payloads (validated against SharePoint Online).

Approach 2: Companion text field

Write to the companion Note field using the pipe-delimited Label|TermGuid format:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/items(7)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>
X-HTTP-Method: MERGE
If-Match: *

{
  "f1e2d3c4000500050005000000000005": "Engineering|d4d5e6f7-0003-0003-0003-000000000003"
}

The property name is the companion field's EntityPropertyName. Retrieve the companion field's GUID from the taxonomy field's TextField property, then retrieve the companion field's EntityPropertyName as described in the companion field section above. In testing, it matched the companion field's InternalName. Both MERGE (X-HTTP-Method: MERGE) and item creation (POST to the items collection) accept either approach.

Response — 204 No Content for MERGE; 201 Created for a new item POST.

Writing Multi-Value Term Values (TaxonomyFieldTypeMulti)

When updating the list item entity directly with POST/MERGE, multi-value managed metadata columns must be written through the companion text field — there is no direct JSON array approach that works for this technique:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/items(7)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>
X-HTTP-Method: MERGE
If-Match: *

{
  "f7e8d9c0000600060006000000000006": "Engineering|d4d5e6f7-0003-0003-0003-000000000003;#Platform Engineering|e5e6f7a8-0004-0004-0004-000000000004"
}

Multiple terms are separated by ;# (semicolon-hash). The format for each term is Label|TermGuid.

Direct array approaches are not accepted for direct item entity updates: Sending a JSON array for TaxonomyFieldTypeMulti returns 500 Internal Server Error. Using an @odata.type collection wrapper returns 400 Bad Request. Only the companion text field format with ;# separators works for direct POST/MERGE to the list item entity (validated against SharePoint Online).

Reading Term Values

Response shape for TaxonomyFieldType (single value)

The companion Note field does not appear in the default item response. Include it explicitly using $select:

GET .../_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/items(7)?$select=Category,f1e2d3c4000500050005000000000005
{
  "Category": {
    "Label": "3",
    "TermGuid": "d4d5e6f7-0003-0003-0003-000000000003",
    "WssId": 3
  },
  "f1e2d3c4000500050005000000000005": "Engineering|d4d5e6f7-0003-0003-0003-000000000003"
}

Label quirk: For TaxonomyFieldType (single value), the Label property in the read response returns the WssId as a string (e.g. "3"), not the human-readable term name. To read the actual term label, include the companion Note field in your $select — it returns the pipe-delimited "TermLabel|TermGuid" string.

Response shape for TaxonomyFieldTypeMulti (multi value)

{
  "Tags": [
    { "Label": "Engineering", "TermGuid": "d4d5e6f7-0003-0003-0003-000000000003", "WssId": 3 },
    { "Label": "Platform Engineering", "TermGuid": "e5e6f7a8-0004-0004-0004-000000000004", "WssId": 5 }
  ]
}

For TaxonomyFieldTypeMulti (multi value), Label returns the actual term name — the opposite behavior from single-value columns. This inconsistency is inherent to SharePoint's REST implementation (validated against SharePoint Online).

Direct $filter expressions on managed metadata columns are not supported. Filtering on a TaxonomyFieldType or TaxonomyFieldTypeMulti column returns 400 Bad Request with message "The field of type TaxonomyFieldType cannot be used in the query filter expression."

Clearing Term Values

Clear a single-value column: Set the field to null:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/items(7)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>
X-HTTP-Method: MERGE
If-Match: *

{
  "Category": null
}

Response — 204 No Content

Clear a multi-value column: Set the companion text field to an empty string. Setting the multi-value field itself to null returns 400 — multi-value taxonomy fields are non-nullable collections:

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/items(7)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>
X-HTTP-Method: MERGE
If-Match: *

{
  "f7e8d9c0000600060006000000000006": ""
}

Response — 204 No Content — the field reads back as an empty array [].

Deleting a Managed Metadata Column

POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists(guid'a1b2c3d4-0001-0001-0001-000000000001')/fields(guid'f1e2d3c4-0005-0005-0005-000000000005')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
X-HTTP-Method: DELETE
If-Match: *
Content-Length: 0

Response — 204 No Content

Deleting a managed metadata column automatically deletes its companion Note field — no separate step is needed.


Quick Reference

Headers

Authorization: Bearer <token> is required on all raw HTTP OAuth examples in this post. When using SPHttpClient in SPFx, authentication is handled automatically.

Term store (/_api/v2.1/termstore)

Header Notes
OData-Version: 4.0 Used throughout this post to explicitly request OData v4 behavior.
Accept: application/json;odata.metadata=none Used throughout this post to request JSON without OData metadata.
Content-Type: application/json Required on requests with a JSON body. application/json;odata.metadata=none is also accepted (validated against SharePoint Online).

Classic SharePoint REST (/_api/...)

Header Notes
OData-Version: 4.0 This post uses OData v4 for all classic /_api/ examples. The classic endpoints also accept OData v3 — v4 is not strictly required, but is the recommended choice and is what SPFx's SPHttpClient uses by default.
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.
X-HTTP-Method: MERGE Tunnels a partial update over POST.
X-HTTP-Method: DELETE Tunnels a delete over POST.
If-Match: * Required on MERGE and DELETE tunnels for list items and fields. Bypasses optimistic concurrency.
Content-Length: 0 Required on bodyless DELETE tunnel requests — SharePoint Online returns 411 Length Required without it. Most HTTP clients add it automatically.

Response Status Codes

Operation Status Body
GET term store root, group, set, term 200 OK JSON entity
GET groups, sets (in group), terms collections 200 OK JSON with value array
Create group / set / term (POST) 201 Created Full entity
Update root / group / set / term (PATCH) 200 OK Full updated entity
Delete term or set 204 No Content Empty
Delete empty group 204 No Content Empty
Delete non-empty group 403 Forbidden Error with notAllowed code
POST to termstore/sets (without group) 400 Bad Request Error with apiNotFound code
createFieldAsXml 200 OK Full field entity
Bind field (PATCH SspId/TermSetId) 204 No Content Empty
Delete field 204 No Content Empty
Write term value to item (MERGE) 204 No Content Empty

Permission Requirements

Term Store Structure Operations (/_api/v2.1/termstore)

The token audience is the Office 365 SharePoint Online API (00000003-0000-0ff1-ce00-000000000000). Both delegated and app-only tokens require the TermStore-specific scopes below. These scopes are distinct from the identically named Microsoft Graph scopes.

The table below summarizes the global term-store operations tested in this post; site-local term-store management is not included.

Operation Minimum delegated OAuth scope Minimum application scope
Read term store, groups, sets, terms TermStore.Read.All (SharePoint) TermStore.Read.All (SharePoint)
Create group, set, or term TermStore.ReadWrite.All (SharePoint) TermStore.ReadWrite.All (SharePoint)
Update root, group, set, or term (PATCH) TermStore.ReadWrite.All (SharePoint) TermStore.ReadWrite.All (SharePoint)
Delete term or set TermStore.ReadWrite.All (SharePoint) TermStore.ReadWrite.All (SharePoint)
Delete group TermStore.ReadWrite.All (SharePoint) TermStore.ReadWrite.All (SharePoint)

Delegated write permission note: In SharePoint Online testing, TermStore.ReadWrite.All alone was not sufficient for a delegated caller without a taxonomy-management role — write operations returned 403 Forbidden. Microsoft's taxonomy-role documentation assigns term-set group creation and deletion to the Term Store Administrator role; Group Manager can manage term sets and terms within groups they are assigned to manage; Contributor can create and modify term sets and terms within groups where they have contributor access. The behavior of Microsoft 365 SharePoint Administrator and Global Administrator roles without an explicit taxonomy-role assignment was not tested here. The one exception validated in SharePoint Online testing is adding terms to an open term set (isOpen: true) — a delegated caller with TermStore.ReadWrite.All and no taxonomy role could POST directly to the set's /children endpoint. For the global term-store operations covered in this post, app-only tokens with TermStore.ReadWrite.All succeeded without a taxonomy role and can create, update, and delete global terms, sets, and groups. Site-local taxonomy behaved differently — creating a term set inside a site-local group returned 403 with the app-only configuration tested, as noted in Part 2. For reads, both delegated and app-only tokens with TermStore.Read.All work without any taxonomy role. An SPFx solution using SPHttpClient calls SharePoint using the current user's SharePoint authentication context — it does not use OAuth scope grants in the way raw bearer-token calls do. The taxonomy-role behavior described above applies to raw bearer-token calls; SPFx callers using M365 admin roles without an explicit taxonomy-role assignment should test their specific scenario.

Managed Metadata Column Operations (/_api/web/lists/.../fields and items)

The table below lists the minimum delegated and application permission scopes for classic /_api/ operations. Delegated scopes apply when calling with a Bearer token on behalf of a signed-in user; the minimum user permission level column shows what access the signed-in user needs when using classic SharePoint permissions and modern SharePoint group permissions. Application permissions apply when calling without a signed-in user context (app-only). An SPFx solution using SPHttpClient does not require explicit grants — it calls SharePoint as the current user, whose SharePoint permissions govern the operation. Delegated permission scope requirements were validated against SharePoint Online using a user with site Member access.

Operation Minimum delegated scope Minimum user permission level Minimum application scope
Read fields (GET) AllSites.Read Classic: Read; Group: Visitor Sites.Read.All
Create a managed metadata list column (createFieldAsXml) AllSites.Manage Classic: Edit; Group: Member Sites.Manage.All
Create a managed metadata site column (createFieldAsXml) AllSites.Manage Classic: Edit; Group: Member Sites.Manage.All
Bind field to term set (PATCH SspId/TermSetId) AllSites.Manage Classic: Edit; Group: Member Sites.Manage.All
Delete a managed metadata column AllSites.Manage Classic: Edit; Group: Member Sites.Manage.All
Write term values to items (MERGE) AllSites.Write Classic: Contribute; Group: Member Sites.ReadWrite.All

Note: The application permissions above are tenant-wide grants. For least-privilege access to specific site collections, the SharePoint Sites.Selected permission can be used for both application and delegated tokens. Sites.Selected consent alone does not grant access to any site — an explicit Read, Write, Manage, or FullControl role must be assigned on each target site collection via the Microsoft Graph /sites/{id}/permissions endpoint. For delegated tokens, access is the intersection of the signed-in user's own permissions and the role granted to the application for that site.


Wrapping Up

Managed Metadata spans two distinct REST surfaces, each with its own scope requirements and behaviors. A few things to keep in mind:

  • /_api/v2.1/termstore is the modern SharePoint REST surface used for term store structure operations in this post. Legacy endpoints (/_api/v1.0/termstore, /_api/SP.Taxonomy.Internal.TaxonomyService) returned 404 in SharePoint Online testing. This surface requires TermStore.Read.All or TermStore.ReadWrite.All — not the Sites scopes used for everything else in this series. For delegated writes, in SharePoint Online testing, TermStore.ReadWrite.All scope alone was not sufficient for callers without a taxonomy-management role — see the delegated write permission note for details. The exception is adding terms to open term sets, which worked with scope alone. For the global term-store operations covered in this post, app-only tokens are not subject to this restriction — site-local taxonomy behaved differently, as noted in Part 2.
  • Within the single-geo SharePoint Online environment tested, the term store root entity was independent of the site URL used to call it. Multi-Geo tenants have additional geo-specific taxonomy behavior — metadata created in the default geo is replicated to satellite geos, while satellite-geo metadata is available only in that geo. The groups collection does differ by calling URL: a site-scoped URL surfaces that site's own local term group; the root URL returns global groups and API-created site-local groups. Both contexts return global and system groups.
  • Groups do not cascade-delete their sets. Delete all sets in a group before deleting the group, or the DELETE returns 403. Sets and terms do cascade-delete their children.
  • System groups (type SystemGroup) are protected. Creating sets in them, renaming them, or deleting them all return 403.
  • $expand on term store endpoints is silently ignored — the response returns 200 with the base entity only and no expanded data.
  • Managed metadata columns are created with createFieldAsXml (not a direct POST to fields), which returns 200 rather than 201. Field creation requires AllSites.Manage / Sites.Manage.AllAllSites.Write / Sites.ReadWrite.All is not sufficient.
  • Every managed metadata column has a companion hidden Note field auto-created alongside it and auto-deleted when the taxonomy field is deleted.
  • For single-value columns (TaxonomyFieldType), the read response Label property returns the WssId as a string — not the term's display name. Read the companion Note field to get the human-readable "TermLabel|TermGuid" string.
  • For multi-value columns (TaxonomyFieldTypeMulti), when writing directly to the list item entity via POST/MERGE, use only the companion text field with Label1|Guid1;#Label2|Guid2 format — direct JSON arrays and @odata.type collection wrappers are not accepted for this technique. On read, Label does return the actual term name (inconsistent with single-value behavior).
  • Clear a single-value taxonomy field by setting it to null. Clear a multi-value taxonomy field by setting the companion text field to an empty string — setting the multi-value field itself to null returns 400.
  • Direct $filter expressions on managed metadata columns return 400.
  • Adding a site column to a list via createFieldAsXml with the site column's SchemaXml (including the ID attribute) inherits the term set binding. No separate PATCH step is needed.

Happy coding!