# SharePoint REST API - Working with Search

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:

```plaintext
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.

```http
Accept: application/json;odata=nometadata
Authorization: Bearer <token>
```

For POST requests, also send `Content-Type: application/json`.

> **SPFx note:** `SPHttpClient` uses OData v4 by default and adds an `OData-Version: 4.0` header to every request. Because the Search endpoint rejects that header, you must override it by setting `odata-version` to an empty string and use the OData v3 `odata=nometadata` Accept qualifier:
> 
> ```typescript
> const response = await this.context.spHttpClient.get(
>   searchUrl,
>   SPHttpClient.configurations.v1,
>   {
>     headers: {
>       Accept: "application/json;odata=nometadata",
>       "odata-version": ""
>     }
>   }
> );
> ```

* * *

## 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:

```json
{
  "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.

```http
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)**

```json
{
  "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.

```http
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:

```http
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):**

```http
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):**

```http
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):**

```http
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):**

```http
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:

```json
{
  "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.

```http
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:**

```http
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:

```http
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):**

```json
{
  "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:

```http
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:

```http
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.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!
