Extracting SharePoint File Version History Using PowerShell


SharePoint keeps every prior version of a file, but the built-in UI only shows you version history one file at a time. If you need it for an audit, a bulk export, or to check who changed what across a whole library, you’re pulling it with a script instead of clicking through the interface. Three ways to do that, depending on whether you’re on SharePoint Online or on-premises.


In this post: Prerequisites · PnP PowerShell (SharePoint Online) · Downloading an old version’s actual content · CSOM for on-premises · REST API with PowerShell · When would you actually use this? · Related reading


Prerequisites
  • Read access to the file/library whose history you’re pulling — you don’t need admin rights just to read versions, only to act on them.
  • For PnP PowerShell: the PnP.PowerShell module installed.
  • For CSOM: the SharePoint Client Components SDK assemblies on the machine running the script (only relevant for on-premises farms).

PnP PowerShell (SharePoint Online)

This is the preferred approach for SharePoint Online — it handles authentication for you and returns version metadata in a few lines:

# Install PnP PowerShell Module (if not installed)
# Install-Module PnP.PowerShell -Scope CurrentUser

# Connect to SharePoint
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/yoursite" -Interactive

# Get file versions
$file = Get-PnPFile -Url "/sites/yoursite/Shared Documents/sample.docx" -AsListItem
$versions = Get-PnPProperty -ClientObject $file -Property Versions

# Export version history to CSV
$versionData = $versions | ForEach-Object {
    [PSCustomObject]@{
        Version = $_.VersionLabel
        ModifiedBy = $_.CreatedBy
        ModifiedDate = $_.Created
    }
}
$versionData | Export-Csv -Path "C:\FileVersionHistory.csv" -NoTypeInformation

Loop this over every file in a library (or every library in a site) and you have a full audit export without touching the UI.


Downloading an old version’s actual content

The export above pulls version metadata — who changed it, when, what the version label was — not the file content itself. That’s fine for an audit log, but not for the “I need what version 3.0 actually said” scenario the use cases below call out. Getting the bytes of a specific historical version needs OpenBinaryStream() on that version object, not just reading its properties:

$file = Get-PnPFile -Url "/sites/yoursite/Shared Documents/sample.docx" -AsListItem
$versions = Get-PnPProperty -ClientObject $file -Property Versions

# Grab a specific version by its label, e.g. "3.0"
$targetVersion = $versions | Where-Object { $_.VersionLabel -eq "3.0" }

$ctx = Get-PnPContext
$stream = $targetVersion.OpenBinaryStream()
$ctx.Load($targetVersion)
Invoke-PnPQuery

$fileBytes = [System.IO.MemoryStream]::new()
$stream.Value.CopyTo($fileBytes)
[System.IO.File]::WriteAllBytes("C:\Recovered\sample-v3.docx", $fileBytes.ToArray())

This is the actual recovery step for “I overwrote the good version” — the metadata export tells you which version to grab, this pulls it down as a real, openable file rather than just confirming it exists in the version history.


CSOM for on-premises

If you’re on SharePoint Server rather than SharePoint Online, PnP PowerShell’s cmdlets don’t apply the same way — CSOM (Client-Side Object Model) is the more reliable path, and it works without needing direct access to the SQL back end:

# Load SharePoint CSOM Assemblies
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"

$siteUrl = "https://yourserver/sites/yoursite"
$fileUrl = "/sites/yoursite/Shared Documents/sample.docx"

$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$ctx.Credentials = [System.Net.CredentialCache]::DefaultCredentials

$file = $ctx.Web.GetFileByServerRelativeUrl($fileUrl)
$versions = $file.Versions
$ctx.Load($versions)
$ctx.ExecuteQuery()

$versions | ForEach-Object {
    [PSCustomObject]@{
        Version = $_.VersionLabel
        ModifiedBy = $_.CreatedBy.LoginName
        ModifiedDate = $_.Created
    }
} | Format-Table -AutoSize

The assembly paths above assume SharePoint 2016/2019 (version “16” in the path) — adjust the version segment if you’re on a different farm version. Swap DefaultCredentials for an explicit SharePointOnlineCredentials object if the script runs somewhere that isn’t already domain-authenticated.


REST API with PowerShell

Works against both SharePoint Online and on-premises, and doesn’t need PnP or CSOM installed at all — just Invoke-RestMethod against the versions endpoint:

$siteUrl = "https://yourtenant.sharepoint.com/sites/yoursite"
$fileServerRelativeUrl = "/sites/yoursite/Shared Documents/sample.docx"
$endpoint = "$siteUrl/_api/web/getfilebyserverrelativeurl('$fileServerRelativeUrl')/versions"

$headers = @{ Accept = "application/json;odata=verbose" }
# For SharePoint Online, authenticate first (e.g. via PnP.PowerShell's Connect-PnPOnline,
# then reuse its access token) -- Invoke-RestMethod alone won't handle SPO auth on its own.

$response = Invoke-RestMethod -Uri $endpoint -Headers $headers -Method Get
$response.d.results | ForEach-Object {
    [PSCustomObject]@{
        Version = $_.VersionLabel
        ModifiedBy = $_.CreatedBy.LoginName
        ModifiedDate = $_.Created
    }
}

The catch with this approach is authentication — SharePoint Online doesn’t accept plain Invoke-RestMethod calls without a bearer token or cookie context, so in practice you’re usually authenticating through PnP PowerShell or MSAL first and reusing that session, which somewhat undercuts the “no dependency” appeal. It’s most worth reaching for when you’re already inside an authenticated context (an Azure Function using a service principal, for example) and just need the raw HTTP call rather than a full library.


PnP PowerShell is the default for SharePoint Online. CSOM is what you reach for on-premises. REST API is for when you’re already authenticated somewhere else and just need the raw call.

When would you actually use this?
  • Compliance or legal needs an export showing who changed a document and when — script it once, run it on a schedule.
  • You accidentally overwrote or deleted content and need to find a specific earlier version fast, across more files than you’d want to click through manually.
  • You’re auditing a library before a migration and need to know how much version bloat (and storage) old versions are actually consuming.


Hope that unblocks you. Comment if you hit something I didn’t cover.


App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

Leave a Comment

Your email address will not be published. Required fields are marked *