How to Fix Vulnerable Plugins, Themes and Core on Many Sites with the xCloud Public API and MCP
Updated November 5, 2026 · 8 min read
You can update only the vulnerable plugins, themes and WordPress core on 1 to 100 sites with one request: POST /api/v1/vulnerabilities/fix from a script, or the matching tool in any AI agent connected to xCloud MCP. xCloud backs up each site first, then updates just the items with an open vulnerability. A dry run shows the plan, and nothing runs until you confirm. You then read each finding’s result from GET /api/v1/sites/bulk-operations/{uuid}.
Use it when a vulnerability report lands for a plugin you run on many client sites, or to clear every critical finding across your team in one pass. Other plugins with updates waiting are not touched.
What the bulk fix updates
The bulk fix works from the same list as GET /api/v1/vulnerabilities, the team-wide list of open vulnerabilities found by each server’s Vulnerability Scan (Wordfence) and by Site Security PRO (Patchstack). For each site with a match, xCloud:
- Takes a backup of the site (on by default).
- Updates each vulnerable plugin, theme and WordPress core to the newest version WordPress offers.
- Checks the site again and reports each finding as
updated,resolvedorneeds_manual_update.
A site can be fixed when it is a provisioned WordPress site (not a Docker app) on a connected server, and the server’s Vulnerability Scan covers it. Any other site is skipped and the rest still run.
Prerequisites
- An xCloud API token with
write:sitesto preview and submit, andread:sitesto read the results. See How to Access the xCloud API to create one. - A team role of Team Admin, Server Admin or Site Admin, with the team’s vulnerability scan permission. See Team Roles & Permissions in xCloud.
- For MCP, an AI agent connected to xCloud MCP. See How to Connect xCloud MCP to Your AI Agent.
- Vulnerability Scan turned on for the servers whose sites you want to fix. See Vulnerability Checker.
Step 1: Choose which findings to fix
The request takes the same filters as GET /api/v1/vulnerabilities, so you can list the findings first and send the same filters to fix them:
| Field | Values | Default |
|---|---|---|
severity |
all, critical, high, medium, low, unknown |
all |
source |
all, wordfence, patchstack |
all |
include_ignored |
true or false. Ignored findings are left out unless this is true |
false |
sites |
A list of 1 to 100 site UUIDs, each only once | Every site in the team not ruled out as no_findings, including skipped sites that cannot be fixed, up to 100 |
backup_before_update |
true or false |
true |
severity matches one level exactly: high fixes high findings only, not critical ones. To fix both, send two requests, or leave severity out.
Without sites, the response lists every site in the team that is not ruled out as no_findings, up to 100. Only sites skipped as no_findings are left out. Sites that cannot be fixed stay in the list as skipped and each one takes one of the 100 places: sites that are not WordPress, Docker apps, sites that are not provisioned (not_eligible), sites on a disconnected server (server_not_connected) and sites you have no permission for (not_found). When the list is cut at 100, the response carries the warning more_sites_have_findings, even if fewer than 100 sites are actually fixed. Running the fix again does not always reach the rest, because the same skipped sites fill the places again. In large teams, send sites in batches of up to 100 to be sure each site is included.
Step 2: Preview the fix with a dry run
Send your filters with "dry_run": true. A dry run checks every site and changes nothing: no backup, no update and no operation is created.
export XCLOUD_TOKEN='YOUR_API_TOKEN'
curl -X POST 'https://app.xcloud.host/api/v1/vulnerabilities/fix' \
--header "Authorization: Bearer $XCLOUD_TOKEN" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"severity": "critical",
"sites": [
"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b"
],
"dry_run": true
}'
Expected result: a 200 response. Each site shows queued (it would be fixed) or skipped with a reason. A queued site lists the items it will update in slugs and each matching finding in findings:
{
"success": true,
"message": "Dry run complete. The payload is valid and nothing was created.",
"data": {
"dry_run": true,
"would_create": {
"uuid": null,
"kind": "site.vulnerability_fix",
"status": "dry_run",
"items": [
{
"site_uuid": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"status": "queued",
"reason": null,
"task_uuid": null,
"backup": { "requested": true, "destination": "remote" },
"warnings": [],
"slugs": ["contact-form-7"],
"findings": [
{ "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "slug": "contact-form-7", "source": "wordfence", "severity": "critical", "resolved_slug": "contact-form-7", "from_version": "5.9.2", "status": "queued" }
],
"skipped_findings": [],
"needs_manual_update": false,
"error": null
},
{
"site_uuid": "7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
"status": "skipped",
"reason": "no_findings",
"task_uuid": null,
"backup": { "requested": true, "destination": null },
"warnings": [],
"slugs": [],
"findings": [],
"skipped_findings": [],
"needs_manual_update": false,
"error": null
}
],
"next_actions": []
},
"warnings": []
}
}
reason |
Meaning |
|---|---|
null |
The site will be fixed |
no_findings |
No open vulnerability on the site matches your filters, the server’s Vulnerability Scan does not cover the site, or none of its findings could be matched to an installed plugin or theme (see skipped_findings) |
not_eligible |
The site is not a provisioned WordPress site, or it is a Docker app |
server_not_connected |
The site’s server is not connected to xCloud |
already_fixing |
A vulnerability fix started on this site in the last 30 minutes is still running |
not_found |
The UUID does not exist, you do not have access to that site, or your team role does not include the vulnerability scan permission for that site |
backup.destination shows where the backup goes: remote or local for the site’s backup destination. When it is null and a backup is requested, the site carries the warning no_backup_destination and xCloud takes a local backup instead.
A finding in skipped_findings with the reason slug_unresolved could not be matched to an installed plugin or theme, so it is not updated. Check that item on the site’s Security → Vulnerability Scan page.
Step 3: Submit the fix
Send the same body with "confirm": true instead of dry_run. Add an Idempotency-Key header so the request is safe to retry: if the connection drops and you send it again with the same key, xCloud returns the fix that already exists instead of starting a second one.
curl -X POST 'https://app.xcloud.host/api/v1/vulnerabilities/fix' \
--header "Authorization: Bearer $XCLOUD_TOKEN" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: critical-fix-2026-11-05' \
--data '{
"severity": "critical",
"sites": [
"3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b"
],
"confirm": true
}'
Expected result: a 202 response with the message Bulk vulnerability fix queued., the operation uuid and a next_actions entry that tells you where to read progress:
"next_actions": [
{ "action": "poll", "method": "GET", "url": "/api/v1/sites/bulk-operations/5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e" }
]
A 202 means xCloud accepted the request, not that the sites are already updated. Without "confirm": true (and without "dry_run": true) the request is refused with 422 and the message The confirm field is required.
To fix sites in another team granted to your token, add the X-Team-Id header. See Multi-Team API Tokens and MCP Access.
Step 4: Poll each finding’s result
Read the operation with the URL from next_actions. This call needs read:sites and changes nothing.
curl 'https://app.xcloud.host/api/v1/sites/bulk-operations/5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e' \
--header "Authorization: Bearer $XCLOUD_TOKEN" \
--header 'Accept: application/json'
Expected result: the same shape as the dry run, with each site’s and each finding’s current status. Poll every few seconds until the operation status is no longer running. The backup runs first, so a site can stay queued for a while.
Site status |
Meaning |
|---|---|
queued |
Waiting, or its backup is running |
running |
The updates are running on the site |
done |
The update run finished. Check each finding’s status |
failed |
Nothing was updated, or the run did not finish. error says which |
skipped |
Not fixed. reason says why |
Once a site is done, each finding shows what happened:
Finding status |
Meaning |
|---|---|
updated |
The item now runs a newer version |
resolved |
The vulnerability is no longer open on the site |
needs_manual_update |
WordPress offered no update, so the vulnerable version is still installed |
When any finding on a site is needs_manual_update, the site’s needs_manual_update field is true. This is common for premium plugins whose updates need a license that WordPress could not use. Update those items by hand, for example with the vendor’s download, then check the site’s Vulnerability Scan page again.
The operation status is completed when no site failed, completed_with_failures when some failed and some finished, and failed when sites failed and none finished. Skipped sites do not count as failed, so check each site and finding instead of trusting the operation status alone.
Step 5: Handle failed sites
The error text of a failed site tells you what happened:
error |
What to do |
|---|---|
The backup before the fix failed, so nothing was updated. |
Check the site’s backup destination, then submit that site again with a new Idempotency-Key |
The fix did not complete. Check the site events for details. |
Open the site’s Events page in the dashboard to see the update output. Fix the cause, then submit that site again with a new key |
xCloud never retries a failed fix on its own. Send only the failed site UUIDs in the new request. A site with a fix that started in the last 30 minutes and is still running is skipped with already_fixing. A failed fix does not block the site, so you can submit it again right away.
Fix vulnerabilities with an AI agent over MCP
In an MCP client connected to xCloud, ask in plain language:
Fix all critical vulnerabilities on my WordPress sites.
Do a dry run first and show me which plugins will be updated on each site, then wait for my OK.
The agent calls the vulnerabilities_fix tool. A dry run needs no confirmation. The real fix is marked as a destructive tool, so most MCP clients ask you to approve it before it runs. Check your client’s approval setting. On the compact MCP profile, the agent runs the vulnerabilities.fix operation through xcloud_execute_destructive, and reads the result with sites.bulk-operations.show through xcloud_execute_read, which changes nothing.
Expected result: the agent lists the items it will update and the sites it will skip, waits for your approval if your client asks for it, then reports each finding’s final status, including any that need a manual update.
Troubleshooting
| Response | Cause | Fix |
|---|---|---|
401 |
No bearer token, or the token was revoked | Send Authorization: Bearer <token> with an active token |
403 on submit |
The token lacks write:sites, or your role or permissions do not include vulnerability scans |
Use a token with write:sites and ask the team owner for the vulnerability scan permission |
403 on poll |
The token lacks read:sites, or the vulnerability scan permission |
Use a token with read:sites and ask the team owner for the vulnerability scan permission |
404 on poll |
The operation UUID is wrong or belongs to another team | Use the URL from next_actions with the same team |
409 |
A request with the same Idempotency-Key is still being accepted |
Wait a moment and send it again with the same key |
422 |
Invalid filter or site list, missing confirm, or the Idempotency-Key was already used with a different body |
Fix the body, or use a new key for a different request |
429 |
More than 30 fix requests in a minute | Wait a minute. Put more sites in one request instead of many small ones |
Frequently asked questions
Does the bulk fix update every plugin on my sites?
No. It updates only the plugins, themes and WordPress core that have an open vulnerability matching your filters. Other plugins and themes with updates waiting are left alone. Use the Updates Manager in the dashboard for those.
What does needs_manual_update mean?
WordPress offered no update for that item when the fix ran, so the vulnerable version is still installed. This is common for premium plugins whose updates need a license. Update the item by hand, for example from the vendor’s site, then check the site’s Vulnerability Scan page again.
Is a backup taken before the update?
Yes, by default. xCloud backs up each site before it updates anything, to the site’s backup destination when it has one, or as a local backup when it has none. If that backup fails, nothing on that site is updated. Send backup_before_update: false to skip it.
Why is a site skipped with no_findings?
xCloud has no open vulnerability for that site that matches your filters. The site may have no vulnerabilities, the vulnerable item may be ignored, or the server’s Vulnerability Scan may not cover the site, so xCloud has no findings for it. A site is also skipped with no_findings when none of its findings could be matched to an installed plugin or theme; those findings are listed in skipped_findings.
Next steps
- Read the full request and response format in the xCloud Public API reference, and authentication and rate limits in the xCloud API documentation overview.
- See every security issue across your sites and servers in one place with the Security Center.
If you run into any issues with the bulk vulnerability fix, feel free to reach out to our support team for help.