# SharePoint REST API - Working with Folders in Lists

This post covers folders in SharePoint custom lists. You'll learn how to create folders and subfolders, retrieve folder metadata and contents, update folder metadata, add and update items in folders, delete folders, send folders to the Recycle Bin, and restore folders from the Recycle Bin through the REST API.

---

## Introduction

`SP.Folder` is the REST representation of a folder in a SharePoint list or document library. This post focuses on **custom lists**. Folders in libraries behave differently — see the Working with Folders in Libraries post for that coverage.

When SharePoint creates a folder in a custom list via the UI, it creates two things: a file system folder and a list item with `FSObjType=1` and a folder content type (`0x0120`). The backing list item is what appears in list views and is the target for list-column metadata updates. Folder-specific operations such as retrieving or deleting the folder can target the `SP.Folder` resource directly. The file system folder name and the list-item `Title` initially match, but they can diverge after updates.

For custom lists, folders are created using `AddValidateUpdateItemUsingPath`. This endpoint creates both the folder and the backing list item in a single call and sets the folder name correctly from the supplied `Title` value.

> **Prerequisite:** The list must have `EnableFolderCreation` set to `true` before you can create folders in it. You can verify this with:
>
> ```http
> GET /_api/web/lists/getbytitle('Projects')?$select=EnableFolderCreation
> ```
>
> If `EnableFolderCreation` is `false`, the folder-creation call succeeds but creates a regular list item (`FSObjType=0`) rather than a folder (validated against SharePoint Online). Enable folder creation in the list's **Advanced Settings** in the SharePoint UI, or via `MERGE` on the list with `{"EnableFolderCreation": true}`.

The primary endpoints for reading a folder are the same as for library folders:

```
GET /_api/web/GetFolderByServerRelativeUrl('{serverRelativeUrl}')
GET /_api/web/GetFolderByServerRelativePath(decodedurl='{serverRelativeUrl}')
```

For a typical custom list, a folder URL follows `/sites/<site>/Lists/<listUrlName>/<folderName>`. The list's URL name may differ from its current display title, so use the list's `RootFolder.ServerRelativeUrl` when constructing paths:

```http
GET /_api/web/lists/getbytitle('Projects')/RootFolder?$select=ServerRelativeUrl
```

> **Authentication note:** The raw HTTP examples use a SharePoint access token. With delegated authentication, the token represents a signed-in user and the operation must be permitted by both the delegated scope and that user's SharePoint permissions. With application (app-only) authentication, the token represents the Entra application and SharePoint evaluates its application permissions. 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 and manages request digests for write operations.

---

## Before You Start: Headers and JSON Format

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

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

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

---

## The SP.Folder Entity

The properties you'll use most often when working with list folders:

| Property | Type | Access | Description |
|---|---|---|---|
| `UniqueId` | `Guid` | Read-only | GUID that uniquely identifies the folder. Stable across renames. |
| `Name` | `string` | Read-only | The folder's display name, e.g. `"Active"`. |
| `ServerRelativeUrl` | `string` | Read-only | Server-relative URL of the folder, e.g. `/sites/marketing/Lists/Projects/Active`. |
| `ServerRelativePath` | `SP.ResourcePath` | Read-only | Resource-path representation of the URL. Serializes as `{"DecodedUrl": "/sites/..."}`. |
| `TimeCreated` | `DateTime` | Read-only | Timestamp the folder was created. |
| `TimeLastModified` | `DateTime` | Read-only | Timestamp the folder was last modified. |
| `ItemCount` | `int` | Read-only | Count of direct child items and subfolders in the list folder. Does not include items or subfolders within nested subfolders (validated against SharePoint Online). |
| `Exists` | `bool` | Read-only | Whether the folder exists at the specified URL in response shapes that expose this property. An ordinary missing-folder lookup can return `404`; do not treat `Exists=false` as a universal existence probe. |
| `WelcomePage` | `string` | Read/Write | Folder-relative URL of the folder's welcome page. Typically empty for regular list folders. |

Navigation properties (accessed via `$expand` or as sub-resources):

| Property | Type | Description |
|---|---|---|
| `Files` | `SP.FileCollection` | Immediate child files. Typically empty for custom list folders. |
| `Folders` | `SP.FolderCollection` | Immediate child subfolders. Does not recurse into nested subfolders. |
| `ListItemAllFields` | `SP.ListItem` | The backing list item, including the `Id` needed for MERGE operations. |
| `ParentFolder` | `SP.Folder` | The folder's parent folder. |
| `StorageMetrics` | `SP.StorageMetrics` | Storage usage metrics for the folder and its contents. |

---

## API Operations

> **Note:** The operation examples in this post are independent. Later examples may reuse paths or item IDs after an earlier example renames, updates, or deletes them.

### Add a Folder

Create a folder in the root of a custom list using `AddValidateUpdateItemUsingPath` with `UnderlyingObjectType: 1`. The `FolderPath.DecodedUrl` points to the list root. The `Title` in `formValues` becomes the folder name:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Projects')/AddValidateUpdateItemUsingPath
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "listItemCreateInfo": {
    "FolderPath": {
      "DecodedUrl": "https://contoso.sharepoint.com/sites/marketing/Lists/Projects"
    },
    "UnderlyingObjectType": 1
  },
  "formValues": [
    {"FieldName": "Title", "FieldValue": "Active"}
  ],
  "bNewDocumentUpdate": false
}
```

**Response — 200 OK (abridged)**

```json
{
  "value": [
    {"ErrorCode": 0, "ErrorMessage": null, "FieldName": "Title", "FieldValue": "Active", "HasException": false, "ItemId": 0},
    {"ErrorCode": 0, "ErrorMessage": null, "FieldName": "Id", "FieldValue": "5", "HasException": false, "ItemId": 0}
  ]
}
```

The response is a collection of `SP.ListItemFormUpdateValue` objects, one per `formValues` entry plus an `Id` entry for the new item. In SharePoint Online responses from this endpoint, `ItemId` is `0` — the actual new item ID is in the `Id` entry's `FieldValue`, returned as a string. `HasException: false` and `ErrorMessage: null` indicate per-field success; `ErrorCode: 0` indicates no error.

> **Note:** If any supplied field fails validation, the new item is not committed. Inspect every returned `HasException` value rather than relying on the HTTP 200 alone.

> **Note:** Microsoft documents `FolderPath.DecodedUrl` as a full absolute URL. Server-relative paths (e.g. `/sites/marketing/Lists/Projects`) are also accepted (validated against SharePoint Online). Spaces in folder names remain as literal spaces — do not percent-encode them.

> **Note:** `UnderlyingObjectType: 1` tells SharePoint to create a folder item. Using `0` creates a regular list item inside the folder at that path (see [Add a List Item to a Folder](#add-a-list-item-to-a-folder)).

---

### Add a Subfolder

Create a subfolder inside an existing folder by setting `FolderPath.DecodedUrl` to the parent folder's full URL:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Projects')/AddValidateUpdateItemUsingPath
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "listItemCreateInfo": {
    "FolderPath": {
      "DecodedUrl": "https://contoso.sharepoint.com/sites/marketing/Lists/Projects/Active"
    },
    "UnderlyingObjectType": 1
  },
  "formValues": [
    {"FieldName": "Title", "FieldValue": "2026"}
  ],
  "bNewDocumentUpdate": false
}
```

**Response — 200 OK**

```json
{
  "value": [
    {"ErrorCode": 0, "ErrorMessage": null, "FieldName": "Title", "FieldValue": "2026", "HasException": false, "ItemId": 0},
    {"ErrorCode": 0, "ErrorMessage": null, "FieldName": "Id", "FieldValue": "6", "HasException": false, "ItemId": 0}
  ]
}
```

The parent folder must already exist and must be a proper folder item (`FSObjType=1`). The subfolder's `FileRef` will be `/sites/marketing/Lists/Projects/Active/2026` (validated against SharePoint Online).

---

### Get Folders in a List (Recursive)

To retrieve all folders in a list at any depth, query the list's items and filter by `FSObjType`:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Projects')/items?$filter=FSObjType eq 1&$select=Id,Title,FileRef,FileLeafRef,FSObjType
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

`FSObjType eq 1` matches folder items. `FSObjType eq 0` matches regular list items.

**Response — 200 OK** (abridged)

```json
{
  "value": [
    {
      "FileLeafRef": "Active",
      "FileRef": "/sites/marketing/Lists/Projects/Active",
      "FSObjType": 1,
      "Id": 5,
      "Title": "Active"
    },
    {
      "FileLeafRef": "2026",
      "FileRef": "/sites/marketing/Lists/Projects/Active/2026",
      "FSObjType": 1,
      "Id": 6,
      "Title": "2026"
    }
  ]
}
```

Both the root-level folder and its subfolder are returned. Use `$select` to include any list columns you need. Normal REST paging still applies; follow the continuation link when the result spans multiple pages.

> **Note:** This query is subject to SharePoint's 5,000-item list view threshold. A list containing more than 5,000 items does not automatically make this query fail, but a query that requires SharePoint to process too many items can be throttled. Paging does not bypass the threshold. For large lists, consider querying individual folder paths directly.

---

### Get Subfolders in a Folder (Not Recursive)

To retrieve only the direct child folders of a specific folder, use the `Folders` navigation property:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')/Folders
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

**Response — 200 OK** (abridged)

```json
{
  "value": [
    {
      "Exists": true,
      "ItemCount": 0,
      "Name": "2026",
      "ServerRelativeUrl": "/sites/marketing/Lists/Projects/Active/2026",
      "TimeCreated": "2026-08-08T17:10:00Z",
      "TimeLastModified": "2026-08-08T17:10:00Z",
      "UniqueId": "c3d4e5f6-a7b8-9012-cdef-123456789012"
    }
  ]
}
```

Only direct child folders are returned — subfolders of subfolders are not included.

---

### Get Items in a Folder

To retrieve the items in a specific folder, filter the list's items collection on `FileDirRef`:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Projects')/items?$filter=FileDirRef eq '/sites/marketing/Lists/Projects/Active'&$select=Title,FileDirRef,FSObjType
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

`FileDirRef` contains the server-relative path of the folder an item belongs to. Only immediate children of that folder are returned — items in subfolders have a different `FileDirRef` and are excluded.

**Response — 200 OK** (abridged)

```json
{
  "value": [
    {
      "FileDirRef": "/sites/marketing/Lists/Projects/Active",
      "FSObjType": 0,
      "Title": "Project Alpha"
    }
  ]
}
```

`FSObjType: 0` indicates a regular list item. Subfolders (`FSObjType: 1`) that exist at the same path are also included in the results — add `and FSObjType eq 0` to the filter to exclude them.

---

### Get a Specific Folder

By server-relative URL:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

By server-relative path (recommended for paths containing special characters):

```http
GET /_api/web/GetFolderByServerRelativePath(decodedurl='/sites/marketing/Lists/Projects/Active')
```

**Response — 200 OK**

```json
{
  "Exists": true,
  "ItemCount": 1,
  "Name": "Active",
  "ServerRelativeUrl": "/sites/marketing/Lists/Projects/Active",
  "TimeCreated": "2026-08-08T17:00:00Z",
  "TimeLastModified": "2026-08-08T17:00:00Z",
  "UniqueId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "WelcomePage": ""
}
```

To include the backing list item ID (needed for MERGE operations), expand `ListItemAllFields`:

```http
GET /_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')?$expand=ListItemAllFields&$select=Name,ServerRelativeUrl,ListItemAllFields/Id,ListItemAllFields/Title
```

> **Lookup columns and `ListItemAllFields`:** Lookup field expansion (`$expand=LookupField`) is not available through `ListItemAllFields`. Requesting `$expand=ListItemAllFields,ListItemAllFields/Customer` returns 400 (validated). To filter, sort, or expand lookup columns on folders, use the list items endpoint instead: `/_api/web/lists/getbytitle('Projects')/items?$filter=ContentType eq 'Folder'&$expand=Customer`.

---

### Update Folder Metadata

List column metadata lives on the folder's backing list item, while folder properties such as `WelcomePage` belong to `SP.Folder`. To update list column metadata, first retrieve the list item `Id` via `$expand=ListItemAllFields`, then MERGE that list item.

**Step 1 — get the list item ID:**

```http
GET /_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')?$expand=ListItemAllFields&$select=ListItemAllFields/Id
```

**Step 2 — MERGE the list item:**

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

{
  "Title": "Active Projects"
}
```

**Response — 204 No Content**

Include any writable list column in the body — not just `Title`.

> **Note:** Updating `Title` changes list-item metadata; it does not rename the folder path. To rename the folder, see [Rename a Folder](#rename-a-folder) below.

---

### Rename a Folder

Renaming a list folder's path requires updating `FileLeafRef` on its backing list item. To keep the list-item `Title` synchronized with the folder name, set both `FileLeafRef` and `Title` in the same MERGE.

**Step 1 — get the list item ID:**

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')?$expand=ListItemAllFields&$select=ListItemAllFields/Id
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

**Step 2 — MERGE `FileLeafRef` and `Title`:**

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

{
  "Title": "Current",
  "FileLeafRef": "Current"
}
```

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

After a successful rename, the folder's `ServerRelativeUrl` and `FileRef` reflect the new name. Any hardcoded references to the old URL will break — SharePoint does not redirect from the previous path.

> **Note:** Best practice is to set both `Title` and `FileLeafRef` in the same MERGE — setting only `FileLeafRef` succeeds but leaves `Title` out of sync with the folder name. No `@odata.type` annotation is required when using the `items(<id>)` endpoint (validated against SharePoint Online).

> **Note:** `SP.Folder` also exposes `MoveTo` and the ResourcePath-based `MoveToUsingPath` actions, which can rename or relocate a folder in a single call. `MoveToUsingPath` is preferred when the path may contain `%` or `#`. Neither is covered in this post.

---

### Add a List Item to a Folder

Add a regular list item inside a folder using `AddValidateUpdateItemUsingPath` with `UnderlyingObjectType: 0`. The `FolderPath.DecodedUrl` points to the target folder:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Projects')/AddValidateUpdateItemUsingPath
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/json;odata.metadata=none
Authorization: Bearer <token>

{
  "listItemCreateInfo": {
    "FolderPath": {
      "DecodedUrl": "https://contoso.sharepoint.com/sites/marketing/Lists/Projects/Active"
    },
    "UnderlyingObjectType": 0
  },
  "formValues": [
    {"FieldName": "Title", "FieldValue": "Project Alpha"}
  ],
  "bNewDocumentUpdate": false
}
```

**Response — 200 OK**

```json
{
  "value": [
    {"ErrorCode": 0, "ErrorMessage": null, "FieldName": "Title", "FieldValue": "Project Alpha", "HasException": false, "ItemId": 0},
    {"ErrorCode": 0, "ErrorMessage": null, "FieldName": "Id", "FieldValue": "7", "HasException": false, "ItemId": 0}
  ]
}
```

The item is created inside the specified folder. Its `FileDirRef` will be `/sites/marketing/Lists/Projects/Active`, confirming placement (validated against SharePoint Online).

---

### Update Field Values of an Item in a Folder

Items inside list folders are regular list items. Update their field values with a standard MERGE using the item's `Id`:

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

{
  "Title": "Project Alpha — Phase 2"
}
```

**Response — 204 No Content**

The item's folder placement (`FileDirRef`) is unaffected by a MERGE operation. To retrieve the item's current field values before updating, use a standard GET on `/_api/web/lists/getbytitle('Projects')/items(<id>)`.

---

### Delete a Folder

Delete a folder and all of its contents using a tunneled DELETE on the folder endpoint. The deletion cascades — all nested subfolders and items are removed in a single operation (validated against SharePoint Online).

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
X-HTTP-Method: DELETE
If-Match: *
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

SharePoint Online returns 204 No Content for this operation (validated against SharePoint Online). The folder and all its contents are deleted without being sent to the site's Recycle Bin. Retention policies or eDiscovery holds may still preserve content for compliance purposes. To send the folder to the Recycle Bin instead, see [Send a Folder to the Recycle Bin](#send-a-folder-to-the-recycle-bin) below.

---

### Send a Folder to the Recycle Bin

To remove a folder while preserving a recovery option, use `recycle()` instead of the DELETE tunnel.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Lists/Projects/Active')/recycle()
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 200 OK**

```json
{
  "value": "36e14511-f010-4e8b-8521-20aae5794eba"
}
```

The response body contains the GUID of the new recycle bin item. The folder and its contents can be restored from the site's recycle bin using this ID.

---

### Restore a Folder from the Recycle Bin

To restore a folder that was sent to the Recycle Bin via `recycle()`, use the `restore()` action on the web's `RecycleBin` collection with the GUID returned by `recycle()`:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/RecycleBin('36e14511-f010-4e8b-8521-20aae5794eba')/restore()
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

The folder and its contents are restored to their original location. The GUID is the recycle bin item ID returned by the `recycle()` call — not the folder's own `UniqueId`.

> **Note:** Restore fails if another file or folder with the same name already exists at the original location. Rename or remove the conflicting item before calling `restore()`.

---

## Quick Reference

### Headers

| Header | Notes |
|---|---|
| `Authorization: Bearer <token>` | Required for raw HTTP OAuth examples. SPFx `SPHttpClient` handles authentication automatically. |
| `Accept: application/json;odata.metadata=none` | Required for a JSON response. Omitting it causes SharePoint to return Atom XML. |
| `Content-Type: application/json;odata.metadata=none` | Required on write requests with a JSON body (`AddValidateUpdateItemUsingPath`, MERGE). |
| `OData-Version: 4.0` | Activates OData v4 behavior. |
| `X-HTTP-Method: MERGE` | Tunnels an update over POST. |
| `X-HTTP-Method: DELETE` | Tunnels a delete over POST. |
| `If-Match: *` | Used on list-item MERGE operations to explicitly bypass ETag concurrency checking. Use a specific ETag value to enforce optimistic concurrency instead. If `If-Match` is omitted, SharePoint updates the item regardless of version. |
| `Content-Length: 0` | Required on bodyless POST requests (tunneled DELETE, `recycle()`) — SharePoint Online returns `411 Length Required` without it (validated). Most HTTP clients add it automatically. |

### Response Status Codes

| Operation | Status | Body |
|---|---|---|
| Add a folder or subfolder (`AddValidateUpdateItemUsingPath`) | 200 OK | Field name/value collection |
| Get folders / specific folder | 200 OK | JSON |
| Get items in a folder (`$filter=FileDirRef eq '...'`) | 200 OK | JSON |
| Update folder metadata (MERGE) | 204 No Content | Empty |
| Rename a folder (MERGE `FileLeafRef` + `Title`) | 204 No Content | Empty |
| Add a list item to a folder (`AddValidateUpdateItemUsingPath`) | 200 OK | Field name/value collection |
| Update item field values (MERGE) | 204 No Content | Empty |
| Delete (tunneled DELETE) | 204 No Content | Empty |
| `recycle()` | 200 OK | `{"value":"<guid>"}` |
| Restore a folder from the Recycle Bin (`restore()`) | 204 No Content | Empty |

### 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 permission level column shows the lowest built-in SharePoint permission level that permits the operation, followed by the lowest standard SharePoint group whose default permissions also permit it. Classic permission levels and group membership are not the same thing: Contribute and Read are SharePoint permission levels; Visitor and Member are standard SharePoint groups. In the standard configuration, the Members group is assigned the Edit permission level and the Visitors group the Read permission level. Group permissions can vary by site template and can be customized, so the effective permission level is what ultimately determines access. Application permissions apply when calling without a signed-in user context (app-only). The application permissions in this table are permissions on the Office 365 SharePoint Online API, not the same-named 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. Delegated permission scope requirements were validated against SharePoint Online using a user with site **Member** access. The classic permission requirements in the table come from Microsoft's documentation.

| Operation | Minimum delegated scope | Minimum user permission level | Minimum tenant-wide application permission |
|---|---|---|---|
| Read folders (GET) | `AllSites.Read` | Classic: Read; Group: Visitor | `Sites.Read.All` |
| Get items in a folder (`$filter=FileDirRef`) | `AllSites.Read` | Classic: Read; Group: Visitor | `Sites.Read.All` |
| Create a folder or subfolder | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Update folder metadata | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Add a list item to a folder | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Update field values of an item in a folder | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Delete a folder | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Send a folder to the Recycle Bin (`recycle()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Restore a folder from the Recycle Bin (`restore()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Enable `EnableFolderCreation` | `AllSites.Manage` | Classic: Edit; Group: Member | `Sites.Manage.All` |

> **Note:** The application permissions above are tenant-wide grants. For least-privilege app-only access to specific site collections, the SharePoint **Sites.Selected** application permission can be used instead. Sites.Selected consent alone does not grant access to any site — an explicit Read, Write, Manage, or FullControl role must be assigned on each target site collection.

> **Note:** The restore permission shown assumes the caller is restoring a folder they deleted. Restoring content deleted by another user requires Edit permission.

---

## Wrapping Up

Folders in custom lists are more nuanced than library folders. A few things to keep in mind as you build with the Folders API in lists:

- `EnableFolderCreation` must be `true` on the list before creating folders. If it is `false`, the `AddValidateUpdateItemUsingPath` call succeeds but creates a regular list item (`FSObjType=0`) rather than a folder (validated against SharePoint Online).
- Use `AddValidateUpdateItemUsingPath` with `UnderlyingObjectType: 1` to create folders and subfolders. This creates both the file system folder and the backing list item in a single call and sets the folder name correctly from the `Title` formValue. Point `FolderPath.DecodedUrl` to the list root for top-level folders, or to the parent folder for subfolders.
- `FolderPath.DecodedUrl` accepts a full absolute URL or a server-relative path. Spaces in folder names remain as literal spaces — do not percent-encode them.
- `UnderlyingObjectType: 1` creates a folder; `UnderlyingObjectType: 0` creates a regular list item placed inside the folder at that path.
- `AddValidateUpdateItemUsingPath` returns a flat collection of field name/value pairs — not an `SP.ListItem` entity. In SharePoint Online responses from this endpoint, `ItemId` is `0`; the actual new item ID is in the `Id` entry's `FieldValue`, returned as a string.
- List column metadata lives on the backing list item. Use `$expand=ListItemAllFields` to retrieve the `Id`, then MERGE via `/_api/web/lists/getbytitle('...')/items(<id>)`. To rename a folder, set `FileLeafRef` on the backing list item — also set `Title` in the same MERGE to keep it in sync with the folder name. After a rename, `FileRef` changes and existing references to the old path will break (validated against SharePoint Online).
- Items inside folders are regular list items. Their `FileDirRef` field indicates which folder they belong to. To retrieve items in a specific folder, filter the list's items collection: `$filter=FileDirRef eq '/sites/.../Lists/.../FolderName'`. Only immediate children are returned — items in subfolders have a different `FileDirRef`. A MERGE on an item updates its field values without moving it.
- `ItemCount` counts all direct children — both list items and subfolders. Items within nested subfolders are not counted (validated against SharePoint Online). To enumerate all folders at any depth, query the list's items with `$filter=FSObjType eq 1`.
- The `Folders` navigation property returns only direct child folders — it does not recurse.
- Deleting a folder with the tunneled DELETE cascades to all nested content. The tunneled DELETE does not send the folder to the site's Recycle Bin; use `recycle()` when you need the normal SharePoint recovery path. To restore a recycled folder, use `POST /_api/web/RecycleBin('<guid>')/restore()` with the GUID returned by `recycle()` — responds with 204 No Content and requires `AllSites.Write`.
- For delegated access, read operations require `AllSites.Read`, folder and item write operations require `AllSites.Write`, and modifying list configuration such as `EnableFolderCreation` requires `AllSites.Manage`. For app-only access, reads require `Sites.Read.All`, folder and item writes require `Sites.ReadWrite.All`, and list configuration changes require `Sites.Manage.All`.

Happy coding!
