# SharePoint REST API - Working with Files

This post covers files — the individual documents stored in SharePoint document libraries. You'll learn how to retrieve file metadata and content, upload new files, update content and metadata, check files in and out, copy, move, and delete them through the REST API.

* * *

## Introduction

`SP.File` is the REST representation of a file stored in a SharePoint document library. SharePoint exposes two closely related objects when you work with a file:

*   **The file entity** (`SP.File`) — name, URL, version, check-out state, and other file-level properties
    
*   **The associated list item** (`SP.ListItem`) — the metadata columns (Title, custom fields, and so on) that live on the underlying list item
    

Most `SP.File` properties are read-only. To update metadata, you write to the file's associated list item via `ListItemAllFields`. To update the file content, either re-upload to the same path with `overwrite=true` or use the file's `/$value` endpoint with a tunneled PUT.

The primary access endpoints are:

```plaintext
GET /_api/web/GetFileByServerRelativeUrl('{serverRelativeUrl}')
GET /_api/web/GetFileByServerRelativePath(decodedurl='{serverRelativeUrl}')
GET /_api/web/GetFileById('{uniqueId}')
GET /_api/web/GetFolderByServerRelativeUrl('{folderUrl}')/Files
GET /_api/web/lists/getbytitle('{libraryName}')/RootFolder/Files
```

`GetFileByServerRelativeUrl` and `GetFileByServerRelativePath(decodedurl=...)` return the same entity; the latter is the newer form and handles filenames containing `%` or `#` more reliably. Both are validated against SharePoint Online. ResourcePath APIs use a decoded SharePoint path — the path value represents actual filename characters rather than a SharePoint-encoded form. Normal HTTP URI encoding still applies when constructing the request URL: a `#` in a filename must be encoded as `%23` (a literal `#` causes the HTTP client to treat the rest as a fragment, returning 400), and a `%` must be encoded as `%25` (a bare `%20` in the URL is silently decoded as a space rather than preserving the literal string `%20`). Both were validated against SharePoint Online.

> **Authentication note:** The raw HTTP examples use a SharePoint Bearer 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.File Entity

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

| Property | Type | Access | Description |
| --- | --- | --- | --- |
| `UniqueId` | `Guid` | Read-only | GUID that uniquely identifies the file. |
| `Name` | `string` | Read-only | Filename including extension, e.g. `"report.docx"`. |
| `ServerRelativeUrl` | `string` | Read-only | Server-relative URL of the file, e.g. `/sites/marketing/Documents/report.docx`. |
| `ServerRelativePath` | `SP.ResourcePath` | Read-only | Resource-path representation of the URL. Serializes as `{"DecodedUrl": "/sites/..."}`. |
| `Title` | `string` | Read-only | Optional display title separate from the filename. Updated via the file's list item — see [Update File Metadata](#update-file-metadata). Null when not set. |
| `Length` | `int64` | Read-only | File size in bytes. The JSON value type may vary by access path — treat it as a string in defensive code. |
| `TimeCreated` | `DateTime` | Read-only | Timestamp the file was created. |
| `TimeLastModified` | `DateTime` | Read-only | Timestamp the file was last modified. |
| `MajorVersion` | `int` | Read-only | Current major version number. |
| `MinorVersion` | `int` | Read-only | Current minor version number. |
| `UIVersionLabel` | `string` | Read-only | Human-readable version label, e.g. `"2.0"` or `"1.3"`. |
| `UIVersion` | `int` | Read-only | Implementation-specific numeric version identifier. Treat this value as opaque rather than deriving the version label from it. |
| `CheckOutType` | `int` | Read-only | Check-out state: `2` = not checked out, `0` = checked out (online), `1` = checked out offline. To determine who has the file checked out, use the `CheckedOutByUser` navigation property (validated against SharePoint Online). |
| `CheckInComment` | `string` | Read-only | The comment from the most recent check-in. Empty string when none. |
| `Level` | `byte` | Read-only | Publication level: `1` = published (major version), `2` = draft (minor version), `255` = checked out to the current user. |
| `Exists` | `bool` | Read-only | Whether the file exists at the specified URL. |
| `ETag` | `string` | Read-only | HTTP ETag for optimistic concurrency, e.g. `"{guid},2"`. |

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

| Property | Type | Description |
| --- | --- | --- |
| `ListItemAllFields` | `SP.ListItem` | The associated list item, including all metadata fields. Expand with `$expand=ListItemAllFields`. |
| `Author` | `SP.User` | The user who added the file. |
| `ModifiedBy` | `SP.User` | The user who last modified the file. |
| `CheckedOutByUser` | `SP.User` | The user who currently has the file checked out. `null` when not checked out. |
| `LockedByUser` | `SP.User` | User who owns the file's current lock. `null` when no lock is present. |
| `Versions` | Collection | Previous versions of the file. See [Get Version History](#get-version-history). |

Less frequently used scalar properties:

| Property | Type | Access | Description |
| --- | --- | --- | --- |
| `ContentTag` | `string` | Read-only | Internal content-version tag used to validate document equality. Treat the value as opaque rather than deriving `MajorVersion` or `MinorVersion` from it. |
| `CustomizedPageStatus` | `int` | Read-only | Whether the file is a customized page. |
| `IrmEnabled` | `bool` | Read-only | Whether Information Rights Management is enabled on the file. |
| `LinkingUri` | `string` | Read-only | URI used to link to the file while preserving file identity across operations such as renames. Newer replacement for `LinkingUrl`. |
| `LinkingUrl` | `string` | Read-only | Legacy linking URL for the file. Its identity information can continue to resolve the file after a rename. Empty string when not set. |
| `ListId` | `Guid` | Read-only | GUID of the library containing the file. |
| `SiteId` | `Guid` | Read-only | GUID of the site containing the file. |
| `WebId` | `Guid` | Read-only | GUID of the web containing the file. |

* * *

## API Operations

### Get Files in a Library's Root Folder

Retrieve files directly in a library's root folder (files in subfolders are not included):

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

You can also access files through the list resource:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Documents')/RootFolder/Files
```

**Response — 200 OK** (abridged)

```json
{
  "value": [
    {
      "CheckInComment": "",
      "CheckOutType": 2,
      "ETag": "\"{C7B989FD-062C-4B6F-9BB1-1D00E69FC4F8},2\"",
      "Exists": true,
      "Length": 41,
      "Level": 1,
      "MajorVersion": 2,
      "MinorVersion": 0,
      "Name": "report.docx",
      "ServerRelativeUrl": "/sites/marketing/Documents/report.docx",
      "TimeCreated": "2026-08-01T09:00:00Z",
      "TimeLastModified": "2026-08-05T14:23:11Z",
      "Title": null,
      "UIVersionLabel": "2.0",
      "UniqueId": "c7b989fd-062c-4b6f-9bb1-1d00e69fc4f8"
    }
  ]
}
```

Use `$select` to narrow the properties returned:

```http
GET /_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Documents')/Files?$select=Name,Length,TimeLastModified,CheckOutType
```

* * *

### Get a Single File

By server-relative URL:

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

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

```http
GET /_api/web/GetFileByServerRelativePath(decodedurl='/sites/marketing/Documents/report.docx')
```

By unique ID (SharePoint Online):

```http
GET /_api/web/GetFileById('c7b989fd-062c-4b6f-9bb1-1d00e69fc4f8')
```

**Response — 200 OK** (abridged)

```json
{
  "CheckInComment": "Quarterly review updates",
  "CheckOutType": 2,
  "ETag": "\"{C7B989FD-062C-4B6F-9BB1-1D00E69FC4F8},2\"",
  "Exists": true,
  "IrmEnabled": false,
  "Length": 41,
  "Level": 1,
  "MajorVersion": 2,
  "MinorVersion": 0,
  "Name": "report.docx",
  "ServerRelativeUrl": "/sites/marketing/Documents/report.docx",
  "TimeCreated": "2026-08-01T09:00:00Z",
  "TimeLastModified": "2026-08-05T14:23:11Z",
  "Title": "Q3 Marketing Report",
  "UIVersion": 1024,
  "UIVersionLabel": "2.0",
  "UniqueId": "c7b989fd-062c-4b6f-9bb1-1d00e69fc4f8"
}
```

The response includes the file's scalar properties. To include metadata fields, expand `ListItemAllFields`:

```http
GET /_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')?$expand=ListItemAllFields
```

> **Lookup columns and** `ListItemAllFields`**:** Lookup field expansion (`$expand=LookupField`) is not available through `ListItemAllFields`. Requesting `$expand=ListItemAllFields,ListItemAllFields/Customer` returns 400 (validated). The same applies when using the `ListItemAllFields` sub-resource directly. To filter, sort, or expand lookup columns on documents, use the list items endpoint instead: `/_api/web/lists/getbytitle('Documents')/items?$expand=Customer,File`.

* * *

### Get File Content

Append `/$value` to retrieve the raw file bytes:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/$value
OData-Version: 4.0
Authorization: Bearer <token>
```

**Response — 200 OK**

The response body is the raw file content. SharePoint returns `Content-Type: application/octet-stream` regardless of the file type (validated against SharePoint Online).

* * *

### Upload a File

Upload a file by POST-ing the raw file bytes to the containing folder's `Files/add(...)` endpoint. Specify `url` (the target filename) and `overwrite`. Set `overwrite=true` to replace an existing file at that path; `overwrite=false` (or omitting the parameter) returns `400` if a file with that name already exists (validated against SharePoint Online).

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Documents')/Files/add(url='report.docx',overwrite=true)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Type: application/octet-stream
Authorization: Bearer <token>

<raw file bytes>
```

**Response — 200 OK** (note: upload returns 200, not 201)

```json
{
  "CheckInComment": "",
  "CheckOutType": 2,
  "Exists": true,
  "Length": 41,
  "Level": 1,
  "MajorVersion": 1,
  "MinorVersion": 0,
  "Name": "report.docx",
  "ServerRelativeUrl": "/sites/marketing/Documents/report.docx",
  "TimeCreated": "2026-08-08T17:01:35Z",
  "TimeLastModified": "2026-08-08T17:01:35Z",
  "Title": null,
  "UIVersionLabel": "1.0",
  "UniqueId": "c7b989fd-062c-4b6f-9bb1-1d00e69fc4f8"
}
```

The response is the `SP.File` entity for the newly created or replaced file.

> **Note:** `Files/add(url=...)` is a string-based API. If filenames can contain `%` or `#`, use `AddUsingPath` instead — it accepts a `decodedurl` parameter that uses a decoded SharePoint path. HTTP URI encoding still applies in the request URL: encode `#` as `%23` and `%` as `%25` — both validated against SharePoint Online (`%23` correctly produces a file named with `#`; a bare `%20` silently produces a file named with a space rather than the literal string `%20`):
> 
> ```http
> POST /_api/web/GetFolderByServerRelativePath(decodedurl='/sites/marketing/Documents')/Files/AddUsingPath(decodedurl='report%232.docx',overwrite=true)
> Content-Type: application/octet-stream
> 
> <raw file bytes>
> ```
> 
> Microsoft also exposes ResourcePath equivalents for file retrieval (`GetFileByServerRelativePath`), copy (`CopyToUsingPath`), and move (`MoveToUsingPath`).

> **Note:** Microsoft's REST documentation limits the single-request `Files/add` pattern to 2 GB. SharePoint Online itself supports files up to 250 GB, so when using SharePoint REST, files larger than 2 GB must use a chunked upload flow: create an empty stub file at the destination first, then call `StartUpload`, `ContinueUpload`, and `FinishUpload` on that `SP.File`, using the same upload-session GUID throughout the sequence. `CancelUpload` is also available to abort an in-progress session.

* * *

### Update File Content

To replace an existing file's content, you can re-upload to the same path with `overwrite=true`:

```http
POST /_api/web/GetFolderByServerRelativeUrl('/sites/marketing/Documents')/Files/add(url='report.docx',overwrite=true)
Content-Type: application/octet-stream

<updated file bytes>
```

The response is 200 OK with the updated `SP.File` entity.

SharePoint also exposes a dedicated content-update pattern using the file's `/$value` endpoint with a tunneled PUT:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/$value
OData-Version: 4.0
X-HTTP-Method: PUT
Content-Type: application/octet-stream
Authorization: Bearer <token>

<updated file bytes>
```

**Response — 204 No Content**

Microsoft's REST guidance presents `Files/add` as the upload/creation operation and `/$value` PUT as the dedicated content-update operation for an existing file.

* * *

### Update File Metadata

File metadata (Title, custom columns, and other library fields) lives on the file's associated list item, not on the `SP.File` entity itself. To update it, 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/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')?$expand=ListItemAllFields
```

The response includes a `ListItemAllFields` object with an `Id` property (the integer list item ID).

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

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/lists/getbytitle('Documents')/items(3)
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": "Q3 Marketing Report"
}
```

**Response — 204 No Content**

Changes are immediately reflected on the `SP.File.Title` property in subsequent GET requests (validated against SharePoint Online). Include any writable library column in the body — not just `Title`.

* * *

### Delete a File

A common SharePoint REST pattern for deleting a file is a POST request tunneled with `X-HTTP-Method: DELETE`. `Content-Length: 0` is required because the request has no body.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
X-HTTP-Method: DELETE
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

`If-Match` is not required — SharePoint Online returns 204 whether the header is present or not (validated against SharePoint Online). You can supply a specific ETag value (`If-Match: "{guid},2"`) to enforce optimistic concurrency and prevent deletion if the file has since been modified.

In SharePoint Online testing, this DELETE pattern permanently deleted the file without creating a Recycle Bin entry. This differs from Microsoft's general REST documentation, which states that DELETE operations on recyclable objects result in a recycle operation. Retention policies or eDiscovery holds may still preserve the content for compliance purposes. Use `recycle()` when you explicitly want a recoverable deletion — see [Send a File to the Recycle Bin](#send-a-file-to-the-recycle-bin) below.

* * *

### Send a File to the Recycle Bin

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

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/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 entry (validated against SharePoint Online). Use this GUID to restore the file.

**Restoring the file** from the Recycle Bin:

```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
Authorization: Bearer <token>
Content-Length: 0
```

**Response — 204 No Content** with an empty body.

`restore()` is addressed from the **web's** `RecycleBin` collection (`/_api/web/RecycleBin`), not the library. The GUID in the path is the Recycle Bin entry GUID returned by `recycle()`.

> **Note:** Restore fails if another file or folder with the same name already exists at the original location. It can also fail because of current library constraints such as unique-value or lookup-field validation.

* * *

## Additional Operations

### Check Out a File

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/CheckOut()
OData-Version: 4.0
Accept: application/json
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

After checkout, the file's `CheckOutType` changes from `2` (not checked out) to `0` (checked out online, validated). While checked out, the file is locked for editing by other users.

> **Note:** Observed behavior in SharePoint Online: `CheckOut()`, `CheckIn()`, and `UndoCheckOut()` do not execute correctly when the `Accept` header includes the `odata.metadata=none` format qualifier (`Accept: application/json;odata.metadata=none`). SharePoint accepts the request and returns `204 No Content`, but the operation is not performed — no error or warning is returned. Use `Accept: application/json` (without an OData format qualifier) for these operations. This behavior is not documented by Microsoft; other tested file operations are not affected (validated against SharePoint Online).

* * *

### Check In a File

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/CheckIn(comment='Updated%20figures%20for%20Q3',checkintype=1)
OData-Version: 4.0
Accept: application/json
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

The `checkintype` parameter controls what kind of version is created:

| Value | Type | Description |
| --- | --- | --- |
| `0` | Minor | Creates a minor version (e.g. 1.0 → 1.1). Requires minor versioning to be enabled on the library. |
| `1` | Major | Creates a major version (e.g. 1.0 → 2.0). |
| `2` | Overwrite | Overwrites the current version without incrementing. |

After check-in, `CheckOutType` returns to `2` and the comment is stored in `CheckInComment` (validated against SharePoint Online).

> **Note:** See the note in [Check Out a File](#check-out-a-file) for guidance on the `Accept` header required for this operation.

* * *

### Undo a Check Out

Discard a check out without creating a new version:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/UndoCheckOut()
OData-Version: 4.0
Accept: application/json
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

`CheckOutType` returns to `2`. Unlike `CheckIn`, `UndoCheckOut` does not create a new version (validated against SharePoint Online).

> **Note:** See the note in [Check Out a File](#check-out-a-file) for guidance on the `Accept` header required for this operation.

* * *

### Copy a File

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/copyto(strNewUrl='/sites/marketing/Documents/report-backup.docx',bOverWrite=false)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

The source file remains in place. Set `bOverWrite=true` to replace a file that already exists at the destination path.

> **Note:** `CopyTo` operates within the current site collection — the destination URL cannot point to a different site collection. The caller also needs sufficient write access at the destination.

> **Note:** For source or destination paths containing `%` or `#`, use `GetFileByServerRelativePath` to address the source file and `CopyToUsingPath` with a `ResourcePath` destination.

* * *

### Move or Rename a File

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/moveto(newUrl='/sites/marketing/Documents/archive/report.docx',flags=1)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

The `flags` parameter is a bitmask of `MoveOperations` values. `1` specifies `Overwrite`, which replaces a file that already exists at the destination. To rename a file without moving it, specify a `newUrl` in the same folder with a different filename.

> **Note:** `MoveTo` operates within the current site collection — the destination URL cannot point to a different site collection. The caller also needs sufficient write access at the destination.

> **Note:** For source or destination paths containing `%` or `#`, use `GetFileByServerRelativePath` to address the source file and `MoveToUsingPath` — see [Move a File Using ResourcePath](#move-a-file-using-resourcepath) below.

* * *

### Move a File Using ResourcePath

`MoveToUsingPath` is the ResourcePath-based alternative to `moveto()`. Use it when the source or destination path may contain `%` or `#`. Parameters are passed inline in the URL.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativePath(decodedurl='/sites/marketing/Documents/report.docx')/MoveToUsingPath(DecodedUrl='/sites/marketing/Documents/archive/report.docx',moveOperations=1)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

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

The `moveOperations` parameter is a bitmask. `0` fails if a file already exists at the destination — SharePoint returns `400 Bad Request`. `1` overwrites an existing file at the destination. Values can be combined: `2` allows broken three-sided links, `4` bypasses a shared lock.

> **Note:** `MoveToUsingPath` operates within the current site collection — the destination path cannot point to a different site collection. The caller needs sufficient write access at the destination.

> **Note:** `SP.Folder` exposes the same `MoveToUsingPath` action, using the same `DecodedUrl` parameter and the same URL-inline calling convention. This makes it the preferred move operation for library folders as well — see the Working with Folders in Libraries post.

* * *

### Publish and Content Approval

SharePoint supports two library-level features that control how content reaches its audience:

*   **Publishing** (`publish` / `unPublish`) — available when a library has both major and minor versioning enabled (`EnableMinorVersions: true`). A minor-version draft is submitted for publishing with `publish()`; `unPublish()` withdraws a submitted or approved version back to draft.

*   **Content approval** (`approve` / `deny`) — available when a library has content approval enabled (`EnableModeration: true`). An approver accepts or rejects a pending version.

Both features are independent but commonly used together. All four operations take an optional `comment` parameter inline in the URL and return **204 No Content**.

**Publish**

Submits the current draft for publishing. The file transitions from Draft (`ModerationStatus: 3`) to Pending (`ModerationStatus: 2`). The version label remains at the minor version until the file is approved.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/publish(comment='Ready%20for%20review')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

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

**Unpublish**

Withdraws a published or pending file back to draft status.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/unPublish(comment='Needs%20updates')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

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

> **Note:** The permission floor for `unPublish()` depends on the file's current state. Withdrawing a *Pending* file succeeds with `AllSites.Write` / `Sites.ReadWrite.All`. Withdrawing an *Approved* file (Level 1, `ModerationStatus: 0`) requires `AllSites.Manage` / `Sites.Manage.All` — `AllSites.Write` returns 403 (validated against SharePoint Online).

**Approve**

Approves a pending file. The file advances to its major version label (e.g. `1.0`), `Level` becomes `1` (published), and `ModerationStatus` becomes `0`.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/approve(comment='Approved')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

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

**Deny**

Rejects a pending file. `ModerationStatus` becomes `1` (Rejected) and the file reverts to draft state.

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/deny(comment='Needs%20revision')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

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

> **Note:** `approve()` and `deny()` require `AllSites.Manage` / `Sites.Manage.All` — `AllSites.Write` returns 403 for both (validated against SharePoint Online). The approving user must also have the SharePoint "Approve Items" permission; in the standard group configuration, site Owners have this permission but Members do not.

> **Note:** `ModerationStatus` is accessible on the file's associated list item as `OData__ModerationStatus`. Values: `0` = Approved, `1` = Rejected, `2` = Pending, `3` = Draft.

> **Note:** Unlike `CheckOut()`, `CheckIn()`, and `UndoCheckOut()`, these four operations work correctly with `Accept: application/json;odata.metadata=none` — the format qualifier does not cause silent failure here (validated against SharePoint Online).

* * *

### Get Version History

Retrieve the file's version history via the `Versions` navigation property:

```http
GET https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/Versions
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Authorization: Bearer <token>
```

**Response — 200 OK** (abridged)

```json
{
  "value": [
    {
      "CheckInComment": "",
      "Created": "2026-08-01T09:00:00Z",
      "ID": 512,
      "IsCurrentVersion": false,
      "Length": 38,
      "Size": 38,
      "Url": "_vti_history/512/Documents/report.docx",
      "VersionLabel": "1.0"
    }
  ]
}
```

> **Note:** The `Versions` collection contains only **previous versions** — the current version is not included (validated against SharePoint Online). The `ID` field is the numeric identifier for the version — use it to address a specific version; don't derive it from `VersionLabel`. (In SharePoint Online, major versions have been observed to use IDs in 512-step increments — `512` = 1.0, `1024` = 2.0 — but this is an implementation detail, not a documented API contract.) `Length` and `Size` both represent the file size in bytes for that version.

* * *

### Restore a File Version

To promote a historical version to current, use `RestoreByLabel` on the `Versions` collection, passing the version label from the version history:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/Versions/RestoreByLabel(versionlabel='1.0')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

`RestoreByLabel` creates a new current version whose content is taken from the specified historical version — it does not rewind or remove the intervening history. In SharePoint Online testing with major versions, restoring version 1.0 while the current version was 2.0 produced version 3.0; the original 1.0 and 2.0 entries remained in the version history.

* * *

### Delete a File Version

To permanently delete a specific historical version, use `DeleteByLabel` (by version label) or `DeleteByID` (by the numeric `ID` from the `Versions` collection):

By label:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/Versions/DeleteByLabel(versionlabel='1.0')
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

By numeric ID:

```http
POST https://contoso.sharepoint.com/sites/marketing/_api/web/GetFileByServerRelativeUrl('/sites/marketing/Documents/report.docx')/Versions/DeleteByID(vid=512)
OData-Version: 4.0
Accept: application/json;odata.metadata=none
Content-Length: 0
Authorization: Bearer <token>
```

**Response — 204 No Content**

The specified version is permanently removed from the `Versions` collection — it is not sent to the Recycle Bin (validated against SharePoint Online).

* * *

## Quick Reference

### Headers

| Header | Notes |
| --- | --- |
| `Authorization: Bearer <token>` | Required for the raw HTTP OAuth examples in this post. SPFx `SPHttpClient` handles authentication automatically. |
| `Accept: application/json;odata.metadata=none` | Requests a JSON response with OData metadata suppressed. This is the format used for most JSON responses in this post. Without an Accept header, SharePoint returns Atom XML. |
| `Accept: application/json` | Use for `CheckOut()`, `CheckIn()`, and `UndoCheckOut()`. In SharePoint Online testing, adding the `odata.metadata=none` qualifier caused these actions to return 204 without performing the operation. |
| `Content-Type: application/octet-stream` | Used when the request body contains raw file bytes, including uploads and `/$value` content updates. |
| `Content-Type: application/json;odata.metadata=none` | Used for JSON-body requests in this post. |
| `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 the associated list-item MERGE to explicitly bypass ETag concurrency checking. Use a specific ETag to enforce optimistic concurrency. In SharePoint Online testing, omitting `If-Match` also allowed the update regardless of version. |
| `Content-Length: 0` | Required on bodyless POST requests (`CheckOut()`, `CheckIn()`, `UndoCheckOut()`, `copyto()`, `moveto()`, `MoveToUsingPath()`, `publish()`, `unPublish()`, `approve()`, `deny()`, `recycle()`, `restore()`, `RestoreByLabel()`, `DeleteByLabel()`, `DeleteByID()`, tunneled DELETE) — SharePoint Online returns `411 Length Required` without it (validated). Most HTTP clients add it automatically. |

### Response Status Codes

| Operation | Status | Body |
| --- | --- | --- |
| GET files / single file / file content / versions | 200 OK | JSON (or raw bytes for `/$value`) |
| Upload file (POST) | 200 OK | `SP.File` entity |
| Update file content (`/$value` PUT) | 204 No Content | Empty |
| Update metadata (MERGE on list item) | 204 No Content | Empty |
| `CheckOut()` | 204 No Content | Empty |
| `CheckIn()` | 204 No Content | Empty |
| `UndoCheckOut()` | 204 No Content | Empty |
| `copyto()` | 204 No Content | Empty |
| `moveto()` | 204 No Content | Empty |
| `MoveToUsingPath()` | 204 No Content | Empty |
| `publish()` | 204 No Content | Empty |
| `unPublish()` | 204 No Content | Empty |
| `approve()` | 204 No Content | Empty |
| `deny()` | 204 No Content | Empty |
| Delete (tunneled DELETE) | 204 No Content | Empty |
| `recycle()` | 200 OK | `{"value":"<guid>"}` |
| `RecycleBin('<guid>')/restore()` | 204 No Content | Empty |
| `Versions/RestoreByLabel()` | 204 No Content | Empty |
| `Versions/DeleteByLabel()` / `Versions/DeleteByID()` | 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. The Members group is assigned the Edit permission level by default, and the Visitors group the Read permission level. 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 SharePoint user access | Minimum tenant-wide application permission |
| --- | --- | --- | --- |
| Read files (GET) | `AllSites.Read` | Classic: Read; Group: Visitor | `Sites.Read.All` |
| Upload a file | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Update file content | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Update file metadata | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Check out / check in | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Copy a file (`copyto()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Move a file (`moveto()`, `MoveToUsingPath()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| `publish()` | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| `unPublish()` (Pending file) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| `unPublish()` (Approved file) | `AllSites.Manage` | Classic: Approve Items; Group: Owner | `Sites.Manage.All` |
| `approve()` / `deny()` | `AllSites.Manage` | Classic: Approve Items; Group: Owner | `Sites.Manage.All` |
| Delete a file | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Send a file to the Recycle Bin (`recycle()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Restore from Recycle Bin (`restore()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Restore a file version (`RestoreByLabel()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |
| Delete a file version (`DeleteByLabel()`, `DeleteByID()`) | `AllSites.Write` | Classic: Contribute; Group: Member | `Sites.ReadWrite.All` |

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

> **Note:** Recycle Bin restore permissions depend on additional factors beyond the scope values above: whether the item is in the first- or second-stage Recycle Bin, and whether the restoring user is the one who deleted it. The `AllSites.Write` / Contribute row reflects the first-stage, same-user restore scenario validated for this post. Second-stage (site-collection) Recycle Bin access requires elevated permissions.

* * *

## Wrapping Up

Files are the core content unit in SharePoint document libraries. A few things to keep in mind as you build with the Files API:

*   File content and file metadata are updated through different endpoints. Replace file bytes either by re-uploading to the same path with `overwrite=true` or by using the file's `/$value` endpoint with a tunneled PUT; update metadata fields by MERGE-ing the associated list item via `ListItemAllFields`.
    
*   Upload returns **200 OK**, not 201 — unlike most create operations in the SharePoint REST API.
    
*   `CheckOutType` uses numeric values: `2` = not checked out, `0` = checked out (online), `1` = checked out offline. A checked-in file shows `2`. A newly uploaded file also normally shows `2`, unless the library requires checkout — in that case the new file is initially checked out to the uploader. To determine who has the file checked out, use the `CheckedOutByUser` navigation property.
    
*   Minor and major check-ins increment the file version; an overwrite check-in (`checkintype=2`) does not. `UndoCheckOut` discards the checkout without creating a version.
    
*   `publish()`, `unPublish()`, `approve()`, and `deny()` are library-feature-gated operations. `publish()` and `unPublish()` require `EnableMinorVersions: true` on the library; `approve()` and `deny()` require `EnableModeration: true`. All four return 204 and take an optional `comment` inline in the URL. `publish()` and `unPublish()` (on a Pending file) require only `AllSites.Write`; `unPublish()` on an Approved file, `approve()`, and `deny()` all require `AllSites.Manage` and the SharePoint "Approve Items" permission (validated against SharePoint Online). `ModerationStatus` on the list item tracks state: `0` = Approved, `1` = Rejected, `2` = Pending, `3` = Draft.

*   `moveto()` and `MoveToUsingPath()` both move or rename a file within the same site collection and both require `AllSites.Write`. Use `MoveToUsingPath()` when the source or destination path may contain `%` or `#` — it takes `DecodedUrl` and `moveOperations` as inline URL parameters and returns 204 No Content. `moveOperations=0` fails with 400 if the destination already exists; `moveOperations=1` overwrites. `MoveToUsingPath` is also available on `SP.Folder` with the same calling convention (validated against SharePoint Online).

*   `recycle()` sends the file to the site Recycle Bin and returns the Recycle Bin entry GUID. In SharePoint Online testing, the tunneled DELETE permanently removed the file without a Recycle Bin entry — see [Delete a File](#delete-a-file) for the full discussion. Pass the GUID from `recycle()` to `/_api/web/RecycleBin('<guid>')/restore()` to recover the file — `AllSites.Write` was sufficient for delegated access in the first-stage restore scenario validated in this post.
    
*   The `Versions` navigation property returns only historical versions — the current version is accessible via the file entity itself, not through the collection. To promote a historical version to current, use `Versions/RestoreByLabel(versionlabel='...')` — this creates a new current version from the specified historical version without rewinding the version counter. To permanently delete a specific version, use `Versions/DeleteByLabel(...)` or `Versions/DeleteByID(vid=...)`. Both require `AllSites.Write` for delegated access.
    
*   For delegated access, read operations require `AllSites.Read` and write operations require `AllSites.Write`. For app-only access, reads require `Sites.Read.All` and writes require `Sites.ReadWrite.All`.
    

Happy coding!
