SharePoint doesn’t have one “add attachment” button that works everywhere — which method you reach for depends on whether you’re writing client-side JavaScript, building an SPFx web part, or automating something from a script. The three approaches below all do the same thing: attach a file to a specific list item. The difference is what kind of code you’re already writing when you need it.
In this post: Prerequisites · Using the REST API and JavaScript · Using PnPjs · Using PnP PowerShell · Listing and downloading existing attachments · Re-running the script safely · When would you actually use this? · Related reading
Prerequisites
- Contribute-level (or higher) permission on the list you’re attaching files to — Read access isn’t enough.
- For PnPjs: the library added to the project (
@pnp/spplus@pnp/sp/attachments). - For PnP PowerShell: the
PnP.PowerShellmodule installed and a connection to the site.
Using the REST API and JavaScript
The raw REST endpoint works from any client-side script with zero extra dependencies. Read the file, then POST it to the item’s attachment collection:
const siteUrl = "https://yourtenant.sharepoint.com/sites/yoursite";
const listName = "YourListName";
const itemId = 1; // Replace with your actual item ID
const fileInput = document.getElementById("fileUpload");
fileInput.addEventListener("change", async function () {
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async function (event) {
const fileContent = event.target.result;
const endpoint = `${siteUrl}/_api/web/lists/getbytitle('${listName}')/items(${itemId})/AttachmentFiles/add(FileName='${file.name}')`;
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Accept": "application/json;odata=verbose",
"Content-Type": "application/json;odata=verbose",
"X-RequestDigest": document.getElementById("__REQUESTDIGEST").value,
},
body: fileContent
});
if (response.ok) {
console.log("Attachment added successfully!");
} else {
console.error("Error uploading attachment:", response.statusText);
}
};
reader.readAsArrayBuffer(file);
});
Two things worth knowing: X-RequestDigest is required on every write request as CSRF protection — pull it from the page’s __REQUESTDIGEST hidden field, or from a fresh call to /_api/contextinfo if you’re not rendering inside SharePoint’s own page. And readAsArrayBuffer() is the read mode that matters here — reading the file as text will corrupt anything that isn’t plain text. This approach gives you full control over the request with no library to add, but you own the header and digest handling yourself, which is more code than either option below for the same result.
Using PnPjs
PnPjs wraps the same REST call in promise-based methods, so you’re not managing headers or digests by hand. Current versions (v3/v4) use the spfi() factory instead of the older global sp singleton:
import { spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/attachments";
const sp = spfi().using(SPFx(this.context)); // inside an SPFx web part
async function addAttachment(listName, itemId, file) {
try {
const item = sp.web.lists.getByTitle(listName).items.getById(itemId);
const response = await item.attachmentFiles.add(file.name, file);
console.log("Attachment added:", response);
} catch (error) {
console.error("Error adding attachment:", error);
}
}
// Usage example:
const fileInput = document.getElementById("fileUpload");
fileInput.addEventListener("change", function () {
const file = fileInput.files[0];
if (file) {
addAttachment("YourListName", 1, file);
}
});
If you’ve seen older PnPjs examples that import a global sp object from @pnp/sp/presets/all, that’s the v2 pattern — it still shows up in a lot of older blog posts and Stack Overflow answers, but current PnPjs expects the spfi() factory shown above. The appeal here is less code and built-in error handling for the same result as the REST example — the tradeoff is one more dependency in the project.
Using PnP PowerShell
For automation — bulk uploads, scheduled jobs, migration scripts — PnP PowerShell is the more natural fit than either of the client-side options above:
# Define variables
$siteUrl = "https://yourtenant.sharepoint.com/sites/yoursite"
$listName = "YourListName"
$itemId = 1 # Replace with actual item ID
$filePath = "C:\Path\To\Your\File.txt"
# Connect to SharePoint Online
Connect-PnPOnline -Url $siteUrl -Interactive
# Add Attachment
Add-PnPListItemAttachment -List $listName -Identity $itemId -Path $filePath
Write-Host "Attachment added successfully!"
For more than one file, loop over a list of paths:
$files = @("C:\Path\To\File1.txt", "C:\Path\To\File2.pdf")
foreach ($file in $files) {
Add-PnPListItemAttachment -List $listName -Identity $itemId -Path $file
Write-Host "Added attachment: $file"
}
This is the right tool once attaching files stops being something a user does in a browser and starts being something a script does on a schedule or in bulk — it’s not something you’d wire into a live SPFx web part.
Listing and downloading existing attachments
The other half of this that comes up just as often: finding out what’s already attached to an item, and pulling the files back down. `Get-PnPListItemAttachment` (used above just to check for a name collision) returns the full list, and `Get-PnPFile` retrieves the actual content by server-relative URL:
$attachments = Get-PnPListItemAttachment -List $listName -Identity $itemId
foreach ($attachment in $attachments) {
Get-PnPFile -Url $attachment.ServerRelativeUrl -Path "C:\Downloads" -FileName $attachment.FileName -AsFile -Force
Write-Host "Downloaded: $($attachment.FileName)"
}
Worth knowing for a bulk export or audit script specifically: list item attachments don’t carry the same metadata a document library file does (no version history, no additional columns) — `Get-PnPListItemAttachment` returns just the file name and its URL, nothing more, since attachments were never designed to be a full document-management surface the way a library is.
Re-running the script safely
Worth knowing before this loop above runs against the same items more than once: Add-PnPListItemAttachment has no overwrite parameter. Re-run it against an item that already has an attachment with that exact file name, and it errors instead of quietly replacing the old one — a real problem the first time this script needs to run again after a failed partial run, or as a recurring scheduled job. The fix is checking (and clearing) first:
$fileName = Split-Path $filePath -Leaf
$existing = Get-PnPListItemAttachment -List $listName -Identity $itemId | Where-Object { $_.FileName -eq $fileName }
if ($existing) {
Remove-PnPListItemAttachment -List $listName -Identity $itemId -FileName $fileName -Force
}
Add-PnPListItemAttachment -List $listName -Identity $itemId -Path $filePath
Worth building this check into any script meant to run more than once against the same list — without it, a scheduled job that’s supposed to refresh an attachment on each run just fails on the second run instead of updating anything.
PnPjs is usually the right default for anything running inside SPFx — reach for the raw REST API only when you specifically want to avoid the extra dependency.
When would you actually use this?
- You’re building a custom form or SPFx web part where users attach a file inline — PnPjs, for the cleaner async code and built-in error handling.
- You want a lightweight client-side script with zero extra dependencies — the raw REST API call.
- You’re bulk-uploading attachments to existing items, or attaching files as part of a migration or automation script — PnP PowerShell.
Related reading
- SharePoint Attachments Operations with PnP.js in React — the same attachmentFiles API, wired into a React component instead of a plain script.
- Best Practices in Handling Email Attachments — what to do with an attachment before it ever reaches SharePoint.
That covers this one — comment below if you want more detail.
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


