Skip to main content

Command Palette

Search for a command to run...

SharePoint REST API - Working with Search

Updated
24 min readView as Markdown
SharePoint REST API - Working with Search
R

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 startrow and rowlimit
  • 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 with Sites.Read.All returned 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, use SPHttpClient instead — 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 EnableMultiGeoSearch query property to true and supply a non-empty ClientType. For GET requests, EnableMultiGeoSearch is supplied through the properties parameter; for POST requests, it is included in the Properties collection. The optional MultiGeoSearchConfiguration query 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).
  • SelectProperties is 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, and RefinementFilters — reject the {"results": [...]} wrapper with 400 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 Querytext are 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 the RowLimit value in the response Properties array echoes the originally requested number (validated against SharePoint Online).
  • startrow is 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 by DocId ascending, record the DocId of the last row from each page, and add IndexDocId>{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. Title is not sortable in a default SharePoint Online configuration — attempting to sort by it returns 400 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=nometadatanot 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.All query 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 with Sites.Read.All returned 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.Selected tokens did not work with the Search REST API in SharePoint Online testing — the endpoint returned SearchServiceException: No User or App Context found. The following table summarizes the supported combinations:

Scenario Permission Search visibility
Delegated, broad Sites.Search.All Content the signed-in user can access across the tenant
Delegated, selected sites Sites.Selected + explicit site Read grant Content the user can access within the application's granted sites
App-only, broad Sites.Read.All Broad tenant content available to the application (in the single-geo tenant tested)
App-only, selected sites Sites.Selected + explicit site Read grant Did not work in SharePoint Online testing — returned SearchServiceException: No User or App Context found

Sites.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.Selected token with a Read grant on a specific site collection restricted search index visibility to content in that site: the same query and user with AllSites.Read returned 36 results spanning multiple site collections and OneDrive, while the Sites.Selected token 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.Selected requests to non-granted sites returned 200 OK with an empty collection rather than 403. App-only Sites.Selected requests to non-granted sites returned 401 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 returns 500 Internal Server Error with that header. Omit this header entirely.
  • Use Accept: application/json;odata=nometadata for search — not the OData v4 form. The rest of this series uses Accept: 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 with 400 Bad Request. POST uses a structured SortList array instead.
  • Not all managed properties are sortable. Title is not sortable in a default SharePoint Online configuration. Use LastModifiedTime, Created, Size, Rank, or DocId for 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 startrow up 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 TotalRows depending on the token type. Delegated results are trimmed to what the signed-in user can access; in the single-geo tenant tested, app-only with Sites.Read.All returned results from the entire tenant index.
  • Sites.Selected can constrain cross-site search. In delegated testing, a Sites.Selected token 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 with Sites.Selected versus 36 with AllSites.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, and RefinementFilters) and the bracket notation for sortlist appear 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!