This article will teach you how to upload files to SharePoint using PowerShell.
Prerequisites
Here are the things youโll need.
- A computer with PowerShell 7.4 or later and PnP.PowerShell 3.3.0 installed. Our article uses PowerShell 7 on Windows 11.
- Access to a SharePoint Online document library.
Install the PnP PowerShell Module
You must install the PnP PowerShell module on your computer first. This module provides cmdlets that allow you to authenticate and interact with your SharePoint Online tenant.
- Open PowerShell and run this command:
Install-Module PnP.PowerShell -Scope CurrentUser
- Verify that the module is installed:
Get-InstalledModule PnP.PowerShell
Get-Module PnP.PowerShell -ListAvailable
Register an Entra ID Application for PnP PowerShell. Keep in mind that PnP PowerShell requires your own Microsoft Entra ID app registration for interactive authentication. You can create it directly from PowerShell using the Register-PnPEntraIDAppForInteractiveLogin cmdlet. Run the following command:
Register-PnPEntraIDAppForInteractiveLogin `
-ApplicationName "SharePointFileUpload" `
-Tenant "contoso.onmicrosoft.com"Replace contoso.onmicrosoft.com with your Microsoft Entra tenant name.The command creates an app registration in Microsoft Entra ID and returns the Application (client) ID. Save this value because you will use it with Connect-PnPOnline.ย For example:
$ClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Note that you need sufficient permissions in Microsoft Entra ID to register an app.
Security note. Register-PnPEntraIDAppForInteractiveLogin can config broad delegated permissions for the app. These permissions may exceed what is required for a simple file-upload case. You should review the permissions in Microsoft Entra ID and follow the principle of least privilege.
Certificate-Based Authentication
Certificate-based authentication can be used for unattended automation (such as scheduled tasks, Azure Automation runbooks, CI/CD pipelines, and other cases where interactive sign-in is not possible).
Before using certificate-based authentication, you need to config the Entra ID app for app-only access. Upload the public certificate to the application’s Certificates & secrets โ Certificates section in Microsoft Entra admin center. Then grant the app the required application permissions for SharePoint and grant admin consent.
For least-privilege access to a specific SharePoint site, you should prefer Sites.Selected. Broader permissions such as Sites.FullControl.All should only be used when the automation genuinely requires access to all SharePoint sites.
To config the Entra ID app, follow the steps below:
- Open Microsoft Entra admin center, then switch to App registrations.
- Open the application identified by $ClientId.
- Go to Certificates & secrets โ Certificates.
- Upload the public certificate (.cer) that corresponds to the private key used by the script.
- Go to API permissions and add the required SharePoint application permissions.
- Click Grant admin consent.
For a simple interactive example, you can securely enter the certificate password with Read-Host:
$CertificatePassword = Read-Host "Enter certificate password" -AsSecureString
Connect-PnPOnline `
-Url $SiteUrl `
-ClientId $ClientId `
-Tenant "contoso.onmicrosoft.com" `
-CertificatePath "C:\Certificates\PnP.pfx" `
-CertificatePassword $CertificatePassword
Note that you should avoid hardcoding certificate passwords in scripts. Store secrets securely using Azure Key Vault, SecretManagement, or automation platform secret stores.
Before we try to upload files to SharePoint using PowerShell, letโs prepare the details.
- What is the target SharePoint site URL for the upload?
- In this example, the target SharePoint site URL is:
https://contoso.sharepoint.com/sites/SalesandMarketing
- What is the target document library relative to the site URL?
- The target document library (folder) is DummyDocs.

- What is the location of the files to upload?
- Suppose you have one or more files that you want to upload to a SharePoint document library. For example, I have ten files under the ./temp folder.

Upload Files to SharePoint Using PowerShell
Now that you have all the details letโs dive into the upload process.
Open PowerShell and store the following variables.
# Microsoft Entra ID application (client) ID
$ClientId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# Target SharePoint Online site
$spoSite = 'https://<tenant>.sharepoint.com/sites/SalesandMarketing/'
# Target document library or folder
$spoDocLibrary = 'DummyDocs'
# Local folder containing the files to upload
$localFolder = './temp'
Now connect to the SharePoint Online site using the Microsoft Entra ID application you registered earlier:
Connect-PnPOnline `
-Url $spoSite `
-ClientId $ClientId `
-Interactive
An interactive Microsoft sign-in window opens. Sign in with an account that has access to the target SharePoint site.
Run this command to ensure youโre connected to the correct SPO site.
Get-PnPSite

Next, confirm that the target folder you specified in the $spoDocLibrary variable exists in the SPO site.
Resolve-PnPFolder -SiteRelativePath $spoDocLibrary
The result below confirms that the target folder exists.

Letโs now gather the files to upload.
$files = Get-ChildItem $localFolder -File
Once the files are loaded into the $files variable, letโs use iteration to upload each file. Weโll use the foreach loop in this example to upload each file.
What ultimately uploads the files to the target is the Add-PnPFile cmdlet.
foreach ($item in $files) {
Add-PnPFile `
-Path $item.FullName `
-Folder $spoDocLibrary
} Note that the -Values parameter is optional. You should use it when you need to populate SharePoint metadata during upload. Our example does not set the Title column because document libraries may use custom content types and metadata.
Each item appears on the screen as they get uploaded to the document library.

Note. Repeated Add-PnPFile calls can increase request volume and throttling risk for large uploads. You should use PnP batching where supported, and for large-scale migrations consider throttling, retries, resumability, and chunked/upload-session methods.
Large files
Keep in mind that SharePoint Online supports files up to 250 GB, but upload behavior depends on the PnP PowerShell cmdlet and the upload method being used. In case you are working with large files, avoid assuming that a simple Add-PnPFile loop is the best method for bulk/production uploads.
For large-file and bulk-upload case, you should test the upload method with your PnP.PowerShell version and consider using PnP’s folder upload functionality or an upload session where appropriate.
Also account for SharePoint Online throttling and transient errors. Our example script includes basic exponential-backoff retries, but production migration tooling should additionally honor Retry-After responses and implement appropriate logging and resumability.
Finally, open the document library in your browser and see the new files you uploaded.

Recursively Upload Files to SharePoint using PowerShell
In the previous section, we demonstrated uploading files under a folder. What if you need to upload files recursively and want to keep the folder structure?
For example, the below screenshot shows that the ./temp folder has a child folder called subdir1. Both the parent and child folders contain ten files each.

The goal is to upload these folders and files to the SharePoint document library, retaining the sub-directory structure.
Because the SharePoint connection and variables are already configured, you can continue with the recursive upload.
Once connected, letโs first upload the files on the parent level. The below command is the same one we ran in the previous section to upload the files from one folder.
$files = Get-ChildItem $localFolder -File
foreach ($item in $files) {
Add-PnPFile `
-Path ($item.FullName.ToString()) `
-Folder $spoDocLibrary `
-Values @{"Title" = $($item.Name) }
} 
And that takes care of the parent folder upload.

But whereโs the subfolder? Thatโs what weโll upload next.
Run this following command to get all child folders under the $localFolder location.
# Get all child folders
$childFolders = Get-ChildItem -Path $localFolder -Directory -Recurse
# Resolve the local base path once
$localFolderPath = (Resolve-Path $localFolder).Path
Now, run this command to upload the files recursively. You do not have to pre-create the subfolders in the document library; the Add-PnPFile cmdlet already does that for you.
# Loop through each child folder
foreach ($childFolder in $childFolders) {
# Form the target subfolder name (i.e. "DummyDocs/subdir1")
$RelativePath = ($childFolder.FullName).Replace($localFolderPath, '').Replace('\', '/')
$TargetSubFolderName = "$spoDocLibrary$RelativePath"
# Get all files in the child folder
$files = Get-ChildItem ($childFolder.FullName) -File
# Loop through each file under the child folder.
foreach ($item in $files) {
Add-PnPFile `
-Path ($item.FullName.ToString()) `
-Folder $TargetSubFolderName `
-Values @{"Title" = $($item.Name) }
}
}
Youโll see a similar display as the screenshot below during the file upload.

Once the upload is complete, you can verify that the subfolder was created in the document library.

And the files were also uploaded to the subfolder, as shown below.

Putting It All Together in a Script
So far, weโve demonstrated the steps for uploading files and folders so that you can grasp how the process works (hopefully). Whatโs even better is turning this whole process into a script.
The following script supports both interactive and unattended execution. By default, it uses interactive authentication. For unattended cases such as scheduled tasks, Azure Automation, and CI/CD pipelines, you need to provide certificate-based authentication parameters instead.
For production Azure workloads, you should prefer managed identity/workload identity when supported by the environment because it avoids storing long-lived credentials/certificate files.
So, first save the code below as Start-FolderUpload.ps1 on your computer:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]
$SiteUrl,
[Parameter(Mandatory)]
[string]
$ClientId,
[Parameter(Mandatory)]
[string]
$LocalFolderPath,
[Parameter(Mandatory)]
[string]
$TargetFolderName,
[Parameter()]
[string]
$Tenant,
[Parameter()]
[string]
$CertificatePath,
[Parameter()]
[SecureString]
$CertificatePassword,
[Parameter()]
[switch]
$Recursive
)
function Add-PnPFileWithRetry {
param (
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string]$Folder,
[hashtable]$Values,
[int]$MaxRetries = 5,
[int]$InitialDelaySeconds = 2
)
for ($Attempt = 1; $Attempt -le $MaxRetries; $Attempt++) {
try {
return Add-PnPFile `
-Path $Path `
-Folder $Folder `
-Values $Values `
-ErrorAction Stop
}
catch {
$Exception = $_.Exception
$StatusCode = $null
if ($Exception.Response -and $Exception.Response.StatusCode) {
$StatusCode = [int]$Exception.Response.StatusCode
}
$IsTransient = $StatusCode -in @(429, 503)
if (-not $IsTransient -or $Attempt -eq $MaxRetries) {
throw
}
$Delay = $InitialDelaySeconds * [math]::Pow(2, $Attempt - 1)
$RetryAfter = $null
if ($Exception.Response -and
$Exception.Response.Headers -and
$Exception.Response.Headers['Retry-After']) {
$RetryAfterValue = [string]$Exception.Response.Headers['Retry-After']
# Retry-After can be either delay-seconds or an HTTP date.
$RetryAfterSeconds = 0
if ([int]::TryParse($RetryAfterValue, [ref]$RetryAfterSeconds)) {
$RetryAfter = $RetryAfterSeconds
}
else {
$RetryAfterDate = [DateTimeOffset]::MinValue
if ([DateTimeOffset]::TryParse(
$RetryAfterValue,
[Globalization.CultureInfo]::InvariantCulture,
[Globalization.DateTimeStyles]::AssumeUniversal,
[ref]$RetryAfterDate
)) {
$RetryAfter = [math]::Max(
0,
[math]::Ceiling(
($RetryAfterDate - [DateTimeOffset]::UtcNow).TotalSeconds
)
)
}
}
if ($RetryAfter -ne $null -and $RetryAfter -gt $Delay) {
$Delay = $RetryAfter
}
Start-Sleep -Seconds $Delay
}
}
}
# Ensure that the LocalFolderPath exists. Exit if not.
if (!$(Test-Path $LocalFolderPath)) {
"The LocalFolderPath does not exist." | Out-Default
return $null
}
else {
$LocalFolderPath = (Resolve-Path $LocalFolderPath).Path
}
# Connect to SharePoint Online.
try {
if ($CertificatePath) {
if (-not $Tenant) {
throw "The -Tenant parameter is required when using certificate authentication."
}
if (-not $CertificatePassword) {
throw "The -CertificatePassword parameter is required when using certificate authentication."
}
Connect-PnPOnline `
-Url $SiteUrl `
-ClientId $ClientId `
-Tenant $Tenant `
-CertificatePath $CertificatePath `
-CertificatePassword $CertificatePassword `
-ErrorAction Stop
}
else {
Connect-PnPOnline `
-Url $SiteUrl `
-ClientId $ClientId `
-Interactive `
-ErrorAction Stop
}
}
catch {
Write-Error "SharePoint connection failed: $($_.Exception.Message)"
exit 1
}
# Ensure that the Document Library exists. Exit if not.
try {
$null = Resolve-PnPFolder -SiteRelativePath $TargetFolderName -ErrorAction Stop
}
catch {
$_.Exception.Message | Out-Default
return $null
}
# Store upload results
$UploadResults = @()
# Upload the top-level folder files only.
$Files = Get-ChildItem -Path $LocalFolderPath -File
foreach ($File in $Files) {
# Warn about large files
if ($File.Length -gt 1GB) {
Write-Warning "Large file detected: $($File.FullName) ($([math]::Round($File.Length / 1GB, 2)) GB)"
}
try {
Add-PnPFileWithRetry `
-Path $File.FullName `
-Folder $TargetFolderName `
| Out-Null
$UploadResults += [PSCustomObject]@{
FileName = $File.Name
FullPath = $File.FullName
Status = "Success"
Error = ""
Timestamp = Get-Date
}
"Uploaded File: $($File.FullName)" | Out-Default
}
catch {
$UploadResults += [PSCustomObject]@{
FileName = $File.Name
FullPath = $File.FullName
Status = "Failed"
Error = $_.Exception.Message
Timestamp = Get-Date
}
"Failed: $($File.FullName)" | Out-Default
}
}
# If -Recursive, upload the subfolders and files
if ($Recursive) {
$SubFolders = Get-ChildItem -Path $LocalFolderPath -Directory -Recurse
foreach ($SubFolder in $SubFolders) {
$RelativePath = $SubFolder.FullName.Replace($LocalFolderPath, '').Replace('\', '/')
$SubTargetFolderName = "$TargetFolderName$RelativePath"
$Files = Get-ChildItem -Path $SubFolder.FullName -File
foreach ($File in $Files) {
try {
Add-PnPFileWithRetry `
-Path $File.FullName `
-Folder $SubTargetFolderName `
| Out-Null
$UploadResults += [PSCustomObject]@{
FileName = $File.Name
FullPath = $File.FullName
Status = "Success"
Error = ""
Timestamp = Get-Date
}
"Uploaded File: $($File.FullName)" | Out-Default
}
catch {
$UploadResults += [PSCustomObject]@{
FileName = $File.Name
FullPath = $File.FullName
Status = "Failed"
Error = $_.Exception.Message
Timestamp = Get-Date
}
"Failed: $($File.FullName)" | Out-Default
}
}
}
}
# Export upload results regardless of whether -Recursive was specified.
$UploadResults | Export-Csv `
-Path ".\UploadResults.csv" `
-NoTypeInformation
"Upload log saved to UploadResults.csv" | Out-Default
Parameters of the script
This script has five parameters, four of which are mandatory.
- -ClientId โ The Microsoft Entra ID application (client) ID used for authentication.
- -SiteURL โ The SharePoint Online site URL.
- -LocalFolderPath โ The location on your local computer containing the files to upload.
- -TargetFolderName โ The target folder or document library name.
- -Recursive โ The switch parameter enables the recursive files and folders to upload. Only the files on the parent folder level will be uploaded if not specified.
- –Tenant โ Microsoft Entra tenant name, required for certificate-based authentication.
- –CertificatePath โ Path to the certificate used for unattended authentication.
- –CertificatePassword โ Password protecting the PFX certificate.
Test the script
Letโs put the script to the test.
.\Start-FolderUpload.ps1 `
-ClientId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' `
-SiteUrl 'https://<tenant>.sharepoint.com/sites/sitename' `
-TargetFolderName 'DummyDocs' `
-LocalFolderPath './temp' `
-Recursive
Replace the ClientId value with the Application (client) ID of your Microsoft Entra ID app registration. And watch the magic as it happens!

When uploading large numbers of files, SharePoint Online may temporarily throttle requests/return transient network errors. The script retries failed uploads using exponential backoff. You should consider additionally honoring the service-provided Retry-After value when available for large-scale production migrations.
Run the script with certificate-based authentication
Don’t use Read-Host to enter the certificate password for fully unattended execution. Store the certificate and its secret in a secure secret store such as Azure Key Vault/automation platform’s secret store.
$CertificatePassword = Read-Host "Enter certificate password" -AsSecureString
.\Start-FolderUpload.ps1 `
-ClientId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' `
-Tenant 'contoso.onmicrosoft.com' `
-SiteUrl 'https://<tenant>.sharepoint.com/sites/sitename' `
-TargetFolderName 'DummyDocs' `
-LocalFolderPath 'C:\Temp\Upload' `
-CertificatePath 'C:\Certificates\PnP.pfx' `
-CertificatePassword $CertificatePassword `
-Recursive
Wrapping Up
PowerShell and SharePoint Online interaction through the PnP module is an excellent way to automate multiple tasks. Youโve learned in this post how to get started by uploading files and folders from a local location to a SharePoint Online document library.
Additionally, we provided a working script that you can use to upload files to SharePoint using PowerShell. Optionally, the script lets you specify whether to recursively upload files while maintaining the subfolder structure, giving you additional control.
How do I upload a file to SharePoint Online using PowerShell?
Install the PnP PowerShell module, connect to your SharePoint Online site using Connect-PnPOnline, and upload files with the Add-PnPFile cmdlet. You must specify the local file path and the target document library or folder.
Can I upload multiple files to a SharePoint document library at once?
Yes. You can use Get-ChildItem to collect files from a local folder and then upload them in a loop with Add-PnPFile.
How do I upload an entire folder structure and keep subfolders in SharePoint?
Use a recursive upload script that enumerates child folders and uploads files to corresponding SharePoint folders. The Add-PnPFile cmdlet can automatically create missing subfolders during the upload process.
Do I need to create SharePoint subfolders before uploading files?
No. When uploading recursively, Add-PnPFile can create the target subfolder structure automatically if it does not already exist.
What authentication method should I use for scheduled tasks or automation?
For unattended scenarios such as scheduled tasks, Azure Automation, and CI/CD pipelines, use certificate-based authentication instead of interactive sign-in.
What happens if a file with the same name already exists in SharePoint?
If document library versioning is enabled, SharePoint creates a new version of the file. If versioning is disabled, the existing file may be overwritten without warning.



does this script work with scheduled Tasks?
Are the credentials cached or is the management shell due to aprooval linked with a token?
You must use certificate-based authentication instead of the interactive username+password for unattended use.
I created a script to automatically send files to sharepoint, but it is reporting an error: The command ‘Connect-PnPOnline’ was found in the module ‘PnP.PowerShell’, but it was not possible
load the module.