Live training: next batches coming soon. Join the waitlist:Waitlists open:Developer Training →Developer →Copilot Studio Training →Copilot Studio →

The Security Validation for This Page Is Invalid and Might Be Corrupted (SharePoint Fix)

SharePoint throws “The security validation for this page is invalid and might be corrupted” when a POST arrives without a valid form digest. Get a fresh one from /_api/contextinfo and send it in the X-RequestDigest header:

const site = "https://spguides.sharepoint.com/sites/SPGuides";
const ctx = await fetch(site + "/_api/contextinfo", { method: "POST", headers: { Accept: "application/json;odata=nometadata" } });
const digest = (await ctx.json()).FormDigestValue;
// send it on every POST, MERGE or DELETE
headers: { "X-RequestDigest": digest }

With that header, the same request returns 201 Created instead of 403 Forbidden.

In this tutorial, we’ll reproduce the security validation for this page is invalid error on a real SharePoint Online site, then fix every cause: a missing header, an expired digest, a digest from the wrong site, stale classic pages and server-side code.

What does “the security validation for this page is invalid” mean?

Every SharePoint write needs a form digest: a token tied to you, one site and a time window. It’s SharePoint’s protection against cross-site request forgery.

Create, update and delete calls send it in X-RequestDigest, unless the request carries an OAuth token. If it’s missing, expired or from another site, SharePoint rejects the call with HTTP 403. My SharePoint REST API tutorial covers the read side, which never needs it.

Reproduce the error with a REST POST

I made a small list called SPGDemo Projects on our SPGuides site, with Title and Status columns. Then I ran this in the browser console while signed in to the site. It’s a normal item create, minus the digest:

const siteUrl = "https://spguides.sharepoint.com/sites/SPGuides";
const res = await fetch(siteUrl + "/_api/web/lists/getbytitle('SPGDemo Projects')/items", {
  method: "POST",
  headers: {
    "Accept": "application/json;odata=nometadata",
    "Content-Type": "application/json;odata=nometadata"
  },
  body: JSON.stringify({ Title: "Vendor Contract Renewal", Status: "New" })
});
console.log(res.status, await res.text());

SharePoint answered with a 403 and this body:

403 {"odata.error":{"code":"-2130575251, Microsoft.SharePoint.SPException","message":{"lang":"en-US",
"value":"The security validation for this page is invalid and might be corrupted. Please use your web browser's Back button to try your operation again."}}}

Error code -2130575251 is the digest check. The Back button advice comes from classic form pages, so ignore it for REST calls.

Fix 1: Get the digest from /_api/contextinfo and send X-RequestDigest

Ask the same site for a digest with an empty POST:

const ctx = await fetch(siteUrl + "/_api/contextinfo", {
  method: "POST",
  headers: { "Accept": "application/json;odata=nometadata" }
});
const info = await ctx.json();
console.log(info.FormDigestTimeoutSeconds, info.FormDigestValue);

It printed the lifetime in seconds and the digest itself (shortened here):

1800 0x4B2E...(128 hex characters)...3A092,24 Oct 2026 05:30:18 -0000

Now repeat the create with the header added:

const res2 = await fetch(siteUrl + "/_api/web/lists/getbytitle('SPGDemo Projects')/items", {
  method: "POST",
  headers: {
    "Accept": "application/json;odata=nometadata",
    "Content-Type": "application/json;odata=nometadata",
    "X-RequestDigest": info.FormDigestValue
  },
  body: JSON.stringify({ Title: "Vendor Contract Renewal", Status: "New" })
});
const item = await res2.json();
console.log(res2.status, item.Id, item.Title, item.Status);

This time the console printed the status, ID, title and status of the new item:

201 8 'Vendor Contract Renewal' 'New'

Item 8, Vendor Contract Renewal, showed up in the list right away. The two rows above it came from the PowerShell runs later in this post:

SPGDemo Projects list showing Vendor Contract Renewal created by a REST POST with X-RequestDigest

Fix 2: Read #__REQUESTDIGEST in classic page scripts

Old Script Editor and Content Editor code often posts with jQuery. Classic pages with the default master page keep a valid digest in a hidden field, so add one header:

$.ajax({
  url: siteUrl + "/_api/web/lists/getbytitle('SPGDemo Projects')/items",
  type: "POST",
  contentType: "application/json;odata=verbose",
  data: JSON.stringify({ __metadata: { type: "SP.Data.SPGDemo_x0020_ProjectsListItem" }, Title: "Vendor Contract Renewal" }),
  headers: {
    "Accept": "application/json;odata=verbose",
    "X-RequestDigest": $("#__REQUESTDIGEST").val()   // the line that was missing
  }
});

Check the spelling: two underscores before REQUESTDIGEST. Modern pages don’t run that kind of embedded script, so new code belongs in an SPFx web part. See the jQuery module error in SPFx if you’re porting old jQuery code.

Why does the error appear after the page sits idle?

A digest expires. Our tenant returned FormDigestTimeoutSeconds of 1800, which is 30 minutes. A single-page app that grabs one digest at load and reuses it starts failing half an hour later.

Cache the digest and renew it shortly before it expires:

let cached = { value: "", expires: 0 };

async function getDigest(siteUrl) {
  if (Date.now() < cached.expires) return cached.value;
  const r = await fetch(siteUrl + "/_api/contextinfo", {
    method: "POST",
    headers: { Accept: "application/json;odata=nometadata" }
  });
  const info = await r.json();
  // renew one minute before SharePoint's timeout (1800 seconds by default)
  cached = { value: info.FormDigestValue, expires: Date.now() + (info.FormDigestTimeoutSeconds - 60) * 1000 };
  return cached.value;
}

Call await getDigest(siteUrl) right before every write, and long-lived pages keep working.

Using a digest from a different site

A digest only works on the site that issued it. I took one from the root site’s contextinfo, posted to /sites/SPGuides, and got the same 403.

When your script writes to several sites, call /_api/contextinfo on each target site’s URL, not on the page you’re running from. If you loop through sites, getting all SharePoint Online sites with PnP PowerShell gives you the URL list.

SPHttpClient and PnPjs handle the digest for you

In an SPFx web part, don’t build the header yourself. SPHttpClient adds X-RequestDigest to writes, gets it from contextinfo and caches it:

const url = `${this.context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('SPGDemo Projects')/items`;
const response = await this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
  body: JSON.stringify({ Title: "Vendor Contract Renewal", Status: "New" })
});   // SPHttpClient adds X-RequestDigest for you

PnPjs does the same through its RequestDigest behavior, which the SPFx() behavior includes. The full setup is in displaying list items in SPFx with PnPjs:

const sp = spfi().using(SPFx(this.context));
await sp.web.lists.getByTitle("SPGDemo Projects").items.add({ Title: "Vendor Contract Renewal", Status: "New" });

For REST through SPHttpClient with other controls, see binding list items to a Fluent UI dropdown.

PowerShell: Invoke-PnPSPRestMethod and OAuth calls

Invoke-PnPSPRestMethod runs any REST call over your PnP connection. After installing PnP PowerShell and connecting, I read the digest lifetime and created an item:

$ctx = Invoke-PnPSPRestMethod -Method Post -Url "/_api/contextinfo"
"Digest lifetime: $($ctx.FormDigestTimeoutSeconds) seconds"

$item = @{ Title = "Annual Vendor Audit"; Status = "New" }
$new = Invoke-PnPSPRestMethod -Method Post -Url "/_api/web/lists/getbytitle('SPGDemo Projects')/items" -Content $item
$new | Select-Object Id, Title, Status | Format-Table
Invoke-PnPSPRestMethod POST in PowerShell 7 reading contextinfo and creating a SharePoint list item

The POST went through without any digest code. That’s because PnP signs in with an OAuth access token.

Microsoft’s REST docs say requests authorized with OAuth don’t need the form digest. To prove it, I sent a raw Invoke-WebRequest with only a bearer token:

$token = Get-PnPAccessToken -ResourceTypeName SharePoint
$site  = "https://spguides.sharepoint.com/sites/SPGuides"
$resp  = Invoke-WebRequest -Method Post -Uri "$site/_api/web/lists/getbytitle('SPGDemo Projects')/items" `
    -Headers @{ Authorization = "Bearer $token"; Accept = "application/json;odata=nometadata" } `
    -ContentType "application/json;odata=nometadata" `
    -Body '{"Title":"Security Training Rollout","Status":"In Progress"}'
"HTTP $($resp.StatusCode)"
$data = $resp.Content | ConvertFrom-Json -AsHashtable
"Created item $($data.Id): $($data.Title) ($($data.Status))"
Invoke-WebRequest POST with a bearer token and no X-RequestDigest returning HTTP 201 in SharePoint Online

It returned HTTP 201. So the error is mostly a browser and cookie problem. For bulk work, adding list items with PnP PowerShell is simpler than raw REST.

SharePoint Server: classic forms and AllowUnsafeUpdates

On SharePoint Server, users who leave a classic form open past the timeout and then click Save see this message as a full error page. Reloading the form fixes it. Farm admins can raise the web application’s timeout:

# SharePoint Server only - run in the SharePoint Management Shell
$wa = Get-SPWebApplication "https://intranet.spguides.com"
$wa.FormDigestSettings.Timeout     # 00:30:00 by default
$wa.FormDigestSettings.Timeout = New-TimeSpan -Minutes 60
$wa.Update()

Custom server code that updates items during a POST hits the same check. Many old fixes set web.AllowUnsafeUpdates = true, but Microsoft flags that as a security risk because it turns off the protection. Validate the posted digest instead:

// Server-side code (SharePoint Server farm solutions) - avoid AllowUnsafeUpdates on POST pages
SPUtility.ValidateFormDigest();          // checks the digest posted by the page
using (SPSite site = new SPSite(siteUrl))
using (SPWeb web = site.OpenWeb())
{
    SPListItem item = web.Lists["SPGDemo Projects"].AddItem();
    item["Title"] = "Vendor Contract Renewal";
    item.Update();
}

Farm solutions don’t run in SharePoint Online, so there the fix is always the REST header or an OAuth token.

Here are more SharePoint developer guides you’ll find useful:

Frequently Asked Questions

What causes “the security validation for this page is invalid and might be corrupted”?

A write request (POST, MERGE or DELETE) reached SharePoint without a valid form digest. The X-RequestDigest header is missing, expired, misspelled or from another site.

How do I get the X-RequestDigest value?

Send an empty POST to your site’s /_api/contextinfo endpoint and read FormDigestValue. On classic pages you can also read the #__REQUESTDIGEST hidden field. The Microsoft Learn REST basics show both.

How long is a SharePoint request digest valid?

1800 seconds (30 minutes) by default. The contextinfo response returns the exact value in FormDigestTimeoutSeconds, so renew the digest before that runs out.

Does SPFx need X-RequestDigest?

Not if you use SPHttpClient or PnPjs. The SPHttpClient reference says it adds the digest to write operations and caches it.

Why do my PowerShell REST calls work without the digest?

PnP PowerShell and other OAuth clients send an access token, and SharePoint doesn’t require the form digest on OAuth-authorized requests.