SharePoint REST API - Working with Search

I'm a .NET/M365 developer, trainer, author, MVP & MCT Alumni
This post covers the /_api/search/query and /_api/search/postquery endpoints — the SharePoint REST interface for submitting keyword and KQL queries against the SharePoint search index. It follows on from the earlier posts in this series on lists, list items, fields, views, content types, files, folders, sites, and user profiles. Behaviors marked as validated were tested against SharePoint Online in September 2026 in a single-geo tenant.
Introduction
SharePoint exposes its search index through two REST endpoints:
GET https://<tenant>.sharepoint.com/sites/<site>/_api/search/query?querytext='...'&...
POST https://<tenant>.sharepoint.com/sites/<site>/_api/search/postquery
This is the SharePoint Search REST service (/_api/search/...), not the Microsoft Search API exposed through Microsoft Graph at /search/query; the authentication model, request format, and response shape are different.
Both endpoints query the SharePoint search index. The site segment in the Search REST URL is not, by itself, a hard content boundary: without site-specific search configuration, results can include matching content from other site collections. However, the site URL does establish the search context and can affect the default result source and query rules. Results are security-trimmed for delegated calls (the signed-in user only sees content they have access to). In the single-geo tenant used for testing, app-only Sites.Read.All returned results from across the entire tenant index, including OneDrive for Business content; delegated Sites.Selected can constrain application search visibility to explicitly granted sites. Use an explicit KQL Path: restriction when you need to constrain results to a particular site or library.
Things you can do with the search endpoints:
- Search the SharePoint index using keywords or KQL expressions
- Select specific managed properties to include in each result row
- Sort results by any sortable managed property
- Page through large result sets using
startrowandrowlimit - Scope queries to a site, library, or content type using KQL property filters
- Refine results using faceted metadata such as file type or author
Authentication note: The raw HTTP examples use a SharePoint Bearer token targeting the SharePoint Online resource (
00000003-0000-0ff1-ce00-000000000000), not Microsoft Graph. With delegated authentication, the token represents a signed-in user and results are trimmed to content that user can access. With application (app-only) authentication, the token represents the Entra application — in the single-geo tenant used for testing, app-only search withSites.Read.Allreturned results from the entire tenant index, including OneDrive for Business content. For Entra app-only access to SharePoint REST, Microsoft documents certificate-based authentication; client-secret app-only tokens are not accepted by SharePoint REST. The legacy SharePoint ACS app-only model was retired on November 27, 2023 and stopped working on April 2, 2026. In SPFx, useSPHttpClientinstead — it supplies the current user's SharePoint authentication context.
Multi-Geo note: The cross-site and app-only behavior described in this post was validated in a single-geo SharePoint Online tenant. In a Multi-Geo tenant, each geography has its own search index. To fan a custom delegated Search REST query across geographies, set the
EnableMultiGeoSearchquery property totrueand supply a non-emptyClientType. For GET requests,EnableMultiGeoSearchis supplied through thepropertiesparameter; for POST requests, it is included in thePropertiescollection. The optionalMultiGeoSearchConfigurationquery property can restrict the target geographies. Microsoft does not support app-only authentication for Multi-Geo search.
Before You Start: Headers and JSON Format
The search endpoint does not support OData v4. Sending OData-Version: 4.0 on a search request returns 500 Internal Server Error. Omit this header entirely (validated against SharePoint Online).
The Accept header also behaves differently for search than for other SharePoint REST endpoints. Unlike the rest of this series — which use Accept: application/json;odata.metadata=none (the OData v4 qualifier) — search requires the OData v3 form: Accept: application/json;odata=nometadata. This returns clean JSON with no odata.metadata key (validated against SharePoint Online). Using the OData v4 qualifier (odata.metadata=none) causes the search endpoint to return Atom XML instead of JSON.
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
For POST requests, also send Content-Type: application/json.
Search Result Structure
Both endpoints return the same JSON shape. The top-level object contains:
| Property | Type | Description |
|---|---|---|
ElapsedTime |
int |
Server-side query execution time in milliseconds. |
PrimaryQueryResult |
object | The main query result block. Contains RelevantResults for standard keyword results. |
SecondaryQueryResults |
array | Results from secondary query rules, if any fired. Usually empty. |
SpellingSuggestion |
string |
A suggested spelling correction for the query, if applicable. May be null or an empty string when no suggestion is returned. |
TriggeredRules |
array | IDs of any search query rules that were triggered. |
Properties |
array | Additional response metadata exposed by some search result shapes. |
The PrimaryQueryResult.RelevantResults object contains:
| Property | Type | Description |
|---|---|---|
RowCount |
int |
Number of rows in this response page. |
TotalRows |
int |
Number of matching results after duplicate trimming. For larger result sets this value may be an estimate rather than an exact count; use it as a paging/result-count indicator rather than assuming exact cardinality. |
TotalRowsIncludingDuplicates |
int |
Matching-result count before duplicate trimming. Can be higher than TotalRows when TrimDuplicates is in effect (the default). This value may likewise be approximate for large result sets. |
Properties |
array | Response metadata as SP.KeyValue pairs — includes StartRecord (the zero-based offset of the first row) and RowLimit (the requested overall row limit, which may differ from RowCount if fewer results are available). |
Table |
object | Contains the Rows array of result objects. |
Each entry in Table.Rows has a Cells array of SP.KeyValue objects:
{
"Key": "Title",
"Value": "Annual Report 2025",
"ValueType": "Edm.String"
}
ValueType uses OData EDM type names, including values such as Edm.String, Edm.Int32, Edm.Int64, Edm.Double, Edm.DateTime, Edm.Boolean, and Edm.Guid. Null values have ValueType of "Null" and an empty Value string. In the SharePoint Online environment tested, Edm.DateTime values were returned as locale-formatted strings — for example, 03/15/2025 09:22:44 — rather than ISO 8601. The exact formatting may vary by locale/environment.
In testing, SharePoint Online returned a set of system columns in each row in addition to the properties requested through selectproperties: DocId, Rank, SiteId, WebId, UniqueId, ListId, contentclass, IsExternalContent, OriginalPath, CollapsingStatus, and several identity and rendering columns. These columns could not be suppressed with selectproperties.
API Operations
The examples below can be run independently against any site in your tenant using an appropriate token.
Search (GET)
Submits a search query using URL parameters. All parameter values that are strings must be wrapped in single quotes as OData string literals.
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=10&selectproperties='Title,Path,FileExtension,LastModifiedTime,Author,Size'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
Response — 200 OK (abridged; one row shown)
{
"ElapsedTime": 312,
"PrimaryQueryResult": {
"RelevantResults": {
"RowCount": 3,
"TotalRows": 3,
"TotalRowsIncludingDuplicates": 3,
"Properties": [
{ "Key": "StartRecord", "Value": "0", "ValueType": "Edm.Int32" },
{ "Key": "RowLimit", "Value": "10", "ValueType": "Edm.Int32" }
],
"Table": {
"Rows": [
{
"Cells": [
{ "Key": "Title", "Value": "Annual Report 2025", "ValueType": "Edm.String" },
{ "Key": "Path", "Value": "https://contoso.sharepoint.com/sites/marketing/Shared Documents/Annual Report 2025.docx", "ValueType": "Edm.String" },
{ "Key": "FileExtension", "Value": "docx", "ValueType": "Edm.String" },
{ "Key": "LastModifiedTime", "Value": "03/15/2025 09:22:44", "ValueType": "Edm.DateTime" },
{ "Key": "Author", "Value": "Alex Wilber", "ValueType": "Edm.String" },
{ "Key": "Size", "Value": "245760", "ValueType": "Edm.Int64" },
{ "Key": "DocId", "Value": "5764489646685614492", "ValueType": "Edm.Int64" },
{ "Key": "Rank", "Value": "18.4513", "ValueType": "Edm.Double" }
]
}
]
}
}
},
"SecondaryQueryResults": [],
"SpellingSuggestion": null,
"TriggeredRules": []
}
In testing, the system columns (DocId, Rank, and others listed above) appeared in every row regardless of what selectproperties requested.
Search (POST)
Submits a search query using a JSON request body. POST is preferred for complex queries — the JSON body is easier to read and avoids URL-length limits and encoding issues with special characters.
POST https://contoso.sharepoint.com/sites/marketing/_api/search/postquery
Accept: application/json;odata=nometadata
Content-Type: application/json
Authorization: Bearer <token>
{
"request": {
"Querytext": "annual report",
"RowLimit": 10,
"SelectProperties": ["Title", "Path", "FileExtension", "LastModifiedTime", "Author", "Size"]
}
}
Response — 200 OK (same shape as the GET response above)
Note the differences from GET:
- The body wraps all parameters inside a
"request"key. - Parameter names are PascalCase (
Querytext,RowLimit,SelectProperties). SelectPropertiesis a plain JSON array of strings — not a comma-delimited string and not wrapped in a{"results": [...]}object. In testing, the collection-valued POST properties tested in this post —SelectProperties,SortList, andRefinementFilters— reject the{"results": [...]}wrapper with400 Bad Request: expected StartArray node(validated against SharePoint Online). Note that Microsoft's current Search REST documentation still shows the{"results": [...]}wrapper in some POST examples — that shape does not work in current SharePoint Online.- String values like
Querytextare plain strings — no surrounding single quotes.
Search Schema Basics
Managed-property settings in the SharePoint search schema determine how a property can be used in a query. A property used in a KQL property restriction must be Queryable; a property requested through selectproperties must be Retrievable; a property used for sorting must be Sortable; and a property used as a refiner must be Refinable (and Queryable). The built-in managed properties used in the examples below are already configured appropriately. In SharePoint Online, newly created managed properties cannot be configured as Sortable or Refinable; for custom metadata that needs those capabilities, map the crawled property to one of the predefined reusable managed properties such as RefinableString00, RefinableDate00, or their equivalents. After creating or changing a managed-property mapping, the affected content must be recrawled before the new values are available in search; in SharePoint Online, you can request a reindex of the affected list or library.
Common KQL Query Patterns
The querytext / Querytext parameter accepts a KQL (Keyword Query Language) expression. Common patterns:
| Query | Matches |
|---|---|
annual report |
Items containing both words anywhere in the indexed content |
"annual report" |
Items containing the exact phrase |
title:"annual report" |
Items where the Title managed property contains the exact phrase |
annual* |
Items containing words beginning with "annual" (prefix wildcard) |
annual OR quarterly |
Items containing either word |
annual AND NOT quarterly |
Items containing "annual" but not "quarterly" |
Path:https://contoso.sharepoint.com/sites/marketing |
Items under the specified site or library URL |
IsDocument:1 |
File-based items only (documents, site pages) — excludes list items and sites. Site pages stored in the SitePages library are file-based and are included (validated against SharePoint Online). |
ContentType:<name> |
Keyword match on the content type name property — for example, ContentType:Document matches items whose content type name contains the word "Document". This is a text search, not an equality filter; it will also match custom content types whose names include that word. For the Document content type family specifically, ContentTypeId:0x0101* is more precise, though that prefix also includes page and site content type variants — use with care. |
FileExtension:docx |
Only Word documents |
Author:"Alex Wilber" |
Items authored by the specified person |
Search REST also supports FQL (FAST Query Language) queries when EnableFQL is set to true, but this post focuses on the default KQL query model.
Multiple KQL clauses can be combined:
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='Path:https://contoso.sharepoint.com/sites/marketing+IsDocument:1+FileExtension:docx'&rowlimit=5&selectproperties='Title,Path,LastModifiedTime'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
Note: KQL in URL parameters must be URL-encoded. In the URL above, spaces between clauses are encoded as
+(common in query strings) or%20. When a string value contains an apostrophe, double it inside the OData string literal (for example,O''Brien) before applying any required URL encoding.
Paging Search Results
Use startrow and rowlimit together to page through results. startrow is zero-indexed: startrow=0 (the default) returns the first page, startrow=10 skips the first 10 rows and returns the next page.
First page (rows 1–10):
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=10&startrow=0&selectproperties='Title,Path'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
Second page (rows 11–20):
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=10&startrow=10&selectproperties='Title,Path'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
The paging formula is: startrow = pageNumber * pageSize (where the first page is page 0). In the GET examples above, rowlimit is used as the page size, so the offsets are 0, 10, 20, and so on.
Paging limits:
- Maximum
rowlimit: 500. Values above 500 are silently capped — the response returns 200 OK with at most 500 rows, but theRowLimitvalue in the responsePropertiesarray echoes the originally requested number (validated against SharePoint Online). startrowis supported up to 50,000. For deeper paging, use DocId-based navigation.- For deep paging beyond 50,000 rows — and for better performance on large result sets generally — use
DocId-based navigation: sort byDocIdascending, record theDocIdof the last row from each page, and addIndexDocId>{lastDocId}to the KQL query for the next page.
DocId-based paging (page 1 — sorted by DocId ascending):
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=10&sortlist='DocId:ascending'&selectproperties='Title,DocId'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
Record the DocId value from the last row in the response (available in Cells as the entry with Key: "DocId"). To fetch the next page, append an IndexDocId constraint to the query — IndexDocId is the KQL managed property name used in inequality filters:
DocId-based paging (page 2 — anchored after the last DocId from page 1):
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report+IndexDocId>17592966573689'&rowlimit=10&sortlist='DocId:ascending'&selectproperties='Title,DocId'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
DocId is the response cell key; IndexDocId is the KQL managed property name for the same value. The bracket form shown in Microsoft's pagination documentation (sortlist='[DocId]:ascending') is rejected by current SharePoint Online — use the plain form above (validated against SharePoint Online). DocId-based navigation depends on sorting by DocId; it is not a drop-in replacement when the result set must be traversed in another sort order such as LastModifiedTime.
The same parameters apply in POST. RowLimit controls the overall number of results returned, while RowsPerPage controls the number of results per page:
{
"request": {
"Querytext": "annual report",
"RowLimit": 10,
"StartRow": 10,
"SelectProperties": ["Title", "Path"]
}
}
Sorting Search Results
GET — sort by a managed property:
Use the sortlist parameter with the format 'Property:direction' where direction is ascending or descending. Bracket notation ([Property]:direction) is rejected with 400 Bad Request: Invalid parameter: sortList (validated against SharePoint Online). Note that Microsoft's large-result pagination documentation still recommends the bracket form (for example, sortlist='[docid]:ascending') — that syntax is rejected by current SharePoint Online.
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=5&sortlist='LastModifiedTime:descending'&selectproperties='Title,LastModifiedTime'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
POST — sort using SortList:
POST https://contoso.sharepoint.com/sites/marketing/_api/search/postquery
Accept: application/json;odata=nometadata
Content-Type: application/json
Authorization: Bearer <token>
{
"request": {
"Querytext": "annual report",
"RowLimit": 5,
"SelectProperties": ["Title", "LastModifiedTime"],
"SortList": [
{ "Property": "LastModifiedTime", "Direction": 1 }
]
}
}
Direction is an integer: 0 = ascending, 1 = descending. To sort by multiple properties, add additional objects to the SortList array — SharePoint applies them in order.
Note: Not all managed properties are sortable. Sortable properties include
Rank(the default sort),LastModifiedTime,Created,Size,DocId, and reusable managed properties that support sorting.Titleis not sortable in a default SharePoint Online configuration — attempting to sort by it returns400 Bad Request: Invalid parameter: sortList(validated against SharePoint Online).
Refiners and Refinement Filters
Refiners aggregate result metadata so clients can offer faceted filtering — for example, showing how many results are Word documents, how many are PDFs, or how many are owned by a specific person.
GET — request refiners:
Use the refiners parameter with a comma-separated list of managed property names, wrapped in single quotes:
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=10&refiners='FileExtension,Author'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
The response includes a RefinementResults block alongside RelevantResults inside PrimaryQueryResult. RefinementResults.Refiners is an array of refiner groups — one per requested property. Each group has a Name (the managed property name) and an Entries array. Each entry has:
| Property | Type | Description |
|---|---|---|
RefinementName |
string | Display label for this facet value (e.g. "docx"). |
RefinementValue |
string | Internal value representation. |
RefinementCount |
int | Number of results in this refinement bin, including duplicates. Because TotalRows reflects duplicate trimming when TrimDuplicates is enabled, refiner counts may not reconcile with TotalRows. |
RefinementToken |
string | Opaque token for use in a subsequent refinementfilters query (e.g. "ǂǂ646f6378"). |
Response (abridged — RefinementResults only):
{
"PrimaryQueryResult": {
"RelevantResults": { },
"RefinementResults": {
"Refiners": [
{
"Name": "FileExtension",
"Entries": [
{
"RefinementName": "docx",
"RefinementValue": "docx",
"RefinementCount": 6,
"RefinementToken": "ǂǂ646f6378"
},
{
"RefinementName": "pdf",
"RefinementValue": "pdf",
"RefinementCount": 3,
"RefinementToken": "ǂǂ706466"
}
]
}
]
}
}
}
GET — apply a refinement filter:
The refinementfilters parameter uses SharePoint's refinement-filter syntax, not the KQL expression used by querytext. When filtering on a returned refinement bin, concatenate the managed-property name and the RefinementToken returned by a prior refiner query, as shown below.
To filter results to a specific facet value, pass its RefinementToken in the refinementfilters parameter. Pass the token to your HTTP client exactly as SharePoint returned it and let the client URL-encode the query parameter. Do not pre-encode the ǂ characters before passing the value to a client that performs its own URL encoding — pre-encoding causes them to be double-encoded on the wire. If you are constructing the final URL manually, encode the value exactly once:
GET https://contoso.sharepoint.com/sites/marketing/_api/search/query?querytext='annual+report'&rowlimit=10&refinementfilters='FileExtension:"ǂǂ646f6378"'
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
POST — refiners and refinement filters:
In a POST body, Refiners is a comma-separated string and RefinementFilters is a plain JSON array of filter strings. POST avoids the URL-encoding concern with the ǂ token characters — the JSON body is transmitted as-is and SharePoint reads the literal characters correctly:
POST https://contoso.sharepoint.com/sites/marketing/_api/search/postquery
Accept: application/json;odata=nometadata
Content-Type: application/json
Authorization: Bearer <token>
{
"request": {
"Querytext": "annual report",
"RowLimit": 10,
"SelectProperties": ["Title", "Path", "FileExtension"],
"Refiners": "FileExtension,Author",
"RefinementFilters": ["FileExtension:\"ǂǂ646f6378\""]
}
}
Quick Reference
Headers
| Header | Notes |
|---|---|
Authorization: Bearer <token> |
Required for the raw HTTP OAuth examples in this post. When using SPHttpClient in SPFx, authentication is handled automatically. |
Accept: application/json;odata=nometadata |
Required for a clean JSON response. Use the OData v3 qualifier odata=nometadata — not the OData v4 form ;odata.metadata=none, which causes the search endpoint to return Atom XML instead of JSON. |
Content-Type: application/json |
Required on POST requests. |
OData-Version: 4.0 |
Do not include. The search endpoint returns 500 Internal Server Error when this header is present. |
GET Query Parameters
| Parameter | Type | Description |
|---|---|---|
querytext |
string (OData literal) | Required. The KQL search query, wrapped in single quotes. |
rowlimit |
int | Maximum number of rows returned by the query. Default: 10. Maximum: 500 (higher values are silently capped). |
rowsperpage |
int | Maximum number of results per page. Primarily useful when implementing paging. |
startrow |
int | Zero-based row offset for paging. Default: 0 (first page). Supported up to 50,000; use DocId-based navigation for deeper paging. |
selectproperties |
string (OData literal) | Comma-separated retrievable managed property names to include, wrapped in single quotes (e.g. 'Title,Path,Size'). In testing, SharePoint also returned system columns regardless of selectproperties. |
sortlist |
string (OData literal) | Sort expression wrapped in single quotes. Format: 'Property:direction'. No bracket notation. The property must be Sortable in the search schema. |
trimduplicates |
bool | Whether to deduplicate results. Default: true. Set to false to see raw counts. |
refiners |
string (OData literal) | Comma-separated Refinable (and Queryable) managed property names to use as refiners, wrapped in single quotes (e.g. 'FileExtension,Author'). Adds RefinementResults to the response. |
refinementfilters |
string (OData literal) | Refiner filter expression, wrapped in single quotes. Use RefinementToken values from a prior refiner query (e.g. 'FileExtension:"ǂǂ646f6378"'). Pass the token as returned; let the HTTP client encode it once. |
POST request Properties
| Property | Type | Description |
|---|---|---|
Querytext |
string | Required. The KQL search query (no surrounding single quotes). |
RowLimit |
int | Overall returned-row limit. Default: 10. Maximum: 500. |
RowsPerPage |
int | Number of results per page. |
StartRow |
int | Zero-based row offset for paging. Default: 0. Maximum: 50,000. |
SelectProperties |
string[] | Array of retrievable managed property names to include (plain JSON array, no results wrapper). |
SortList |
object[] | Array of { "Property": "...", "Direction": 0 | 1 } sort objects. 0 = ascending, 1 = descending. The property must be Sortable in the search schema. |
TrimDuplicates |
bool | Whether to deduplicate results. Default: true. |
Refiners |
string | Comma-separated Refinable (and Queryable) managed property names to use as refiners (e.g. "FileExtension,Author"). Adds RefinementResults to the response. |
RefinementFilters |
string[] | Array of refiner filter strings using RefinementToken values (e.g. ["FileExtension:\"ǂǂ646f6378\""]). |
Response Status Codes
| Operation | Status | Body |
|---|---|---|
| Search (GET or POST) | 200 OK | JSON search result object |
Search with OData-Version: 4.0 header |
500 Internal Server Error | Error JSON |
| Search with unsortable property in sortlist | 400 Bad Request | "Invalid parameter: sortList." |
| Search with bracket notation in sortlist | 400 Bad Request | "Invalid parameter: sortList." |
POST with collection property (SelectProperties, SortList, RefinementFilters) as {"results": [...]} |
400 Bad Request | OData error: expected StartArray node |
Permission Requirements
The table below lists the minimum delegated and application permission scopes. Delegated scopes apply when calling with a Bearer token on behalf of a signed-in user; the minimum user access column indicates the SharePoint access the signed-in user requires — the standard Visitors group provides the Read permission level used in the validation tests. Application permissions apply when calling without a signed-in user context (app-only). The delegated and application permissions in this table are permissions on the Office 365 SharePoint Online API, not Microsoft Graph permissions. An SPFx solution using SPHttpClient does not require you to grant these delegated or application scopes to the solution; it calls SharePoint as the current user, whose SharePoint permissions govern the operation. Sites.Search.All was validated as the minimum delegated scope for broad Search REST at Visitor permission level — AllSites.Read is also sufficient but grants broader access beyond just search. Microsoft's documentation identifies Read permission as sufficient user access for search.
| Operation | Delegated permission | Minimum user access | Application permission |
|---|---|---|---|
| Search (GET or POST) — broad | Sites.Search.All |
Read (for example, Visitors) | Sites.Read.All |
| Search (GET or POST) — selected sites | Sites.Selected + site Read grant |
Read (for example, Visitors) | App-only Sites.Selected did not work with Search REST in testing (see note) |
Note: Delegated search results are security-trimmed: the signed-in user sees only content they have permission to access. In testing, the same
Sites.Search.Allquery returned 40 results for a user with Visitor access and 68 for a user with Member access, illustrating that the application's search scope does not override the signed-in user's SharePoint permissions. In the single-geo tenant tested, app-only search withSites.Read.Allreturned tenant-wide results, including OneDrive for Business content across all users.The SharePoint Sites.Selected permission can restrict a delegated application's visibility in search to explicitly granted sites. App-only
Sites.Selectedtokens did not work with the Search REST API in SharePoint Online testing — the endpoint returnedSearchServiceException: No User or App Context found. The following table summarizes the supported combinations:
Scenario Permission Search visibility Delegated, broad Sites.Search.AllContent the signed-in user can access across the tenant Delegated, selected sites Sites.Selected+ explicit siteReadgrantContent the user can access within the application's granted sites App-only, broad Sites.Read.AllBroad tenant content available to the application (in the single-geo tenant tested) App-only, selected sites Sites.Selected+ explicit siteReadgrantDid not work in SharePoint Online testing — returned SearchServiceException: No User or App Context foundSites.Selected and cross-site search: The site collection in the Search REST URL does not define the search boundary — with broad delegated permissions, a query can return matching content from other site collections and OneDrive, subject to the user's access. In delegated testing, a
Sites.Selectedtoken with aReadgrant on a specific site collection restricted search index visibility to content in that site: the same query and user withAllSites.Readreturned 36 results spanning multiple site collections and OneDrive, while theSites.Selectedtoken returned 3 results — all from the granted site. The signed-in user was a tenant administrator, confirming that the application's grant — not the user's personal access — was the limiting factor (validated against SharePoint Online).In separate classic SharePoint REST testing (GET on site lists), delegated
Sites.Selectedrequests to non-granted sites returned200 OKwith an empty collection rather than403. App-onlySites.Selectedrequests to non-granted sites returned401 Unauthorized: Attempted to perform an unauthorized operation. HTTP status alone is not a reliable indicator of whether a Sites.Selected grant exists for a site when using delegated tokens.
Wrapping Up
The SharePoint search REST API is one of the most broadly useful endpoints in the series — in the single-geo tenant used for testing, a single query returned results from across the entire tenant index. A few things to keep in mind:
- OData v4 is not supported. Do not send
OData-Version: 4.0. The search endpoint returns500 Internal Server Errorwith that header. Omit this header entirely. - Use
Accept: application/json;odata=nometadatafor search — not the OData v4 form. The rest of this series usesAccept: application/json;odata.metadata=none(OData v4 qualifier), but search is an exception: the OData v4 qualifier (odata.metadata=none) causes the search endpoint to return Atom XML instead of JSON. Use the OData v3 qualifier (odata=nometadata) to get clean JSON with no metadata key (validated against SharePoint Online). - The Search URL's site segment is not a hard content boundary. It can influence the default result source and query rules, but it does not automatically restrict results to that site's content. Use
Path:when you need explicit content scoping. - GET sortlist does not accept bracket notation. The format is
sortlist='Property:direction'— bracket notation ([Property]:direction) is rejected with400 Bad Request. POST uses a structuredSortListarray instead. - Not all managed properties are sortable.
Titleis not sortable in a default SharePoint Online configuration. UseLastModifiedTime,Created,Size,Rank, orDocIdfor reliable sorting. - Maximum rowlimit is 500. Higher values are silently capped — the response returns 200 OK with at most 500 rows. For large result sets, page using
startrowup to 50,000, then switch to DocId-based navigation. - App-only search can return more results than delegated search. The same query can return very different
TotalRowsdepending on the token type. Delegated results are trimmed to what the signed-in user can access; in the single-geo tenant tested, app-only withSites.Read.Allreturned results from the entire tenant index. - Sites.Selected can constrain cross-site search. In delegated testing, a
Sites.Selectedtoken restricted search results to explicitly granted site collections even when the signed-in user was a tenant administrator with broader access. The same query returned 3 results withSites.Selectedversus 36 withAllSites.Read. - Microsoft's Search REST documentation doesn't always match current SharePoint Online. In particular, the
{"results": [...]}wrapper for POST collection-valued properties (SelectProperties,SortList, andRefinementFilters) and the bracket notation forsortlistappear in Microsoft's current documentation but are rejected by current SharePoint Online. The examples in this post use the shapes accepted by current SharePoint Online.
Happy coding!

