# How to Fix Vulnerable Plugins, Themes and Core on Many Sites with the xCloud Public API and MCP

> Update only the vulnerable plugins, themes and WordPress core on up to 100 sites with POST /api/v1/vulnerabilities/fix or MCP: backup first, dry run, then confirm.

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](/docs/vulnerability-checker-in-xcloud/) (Wordfence) and by [Site Security PRO](/docs/set-up-site-security-pro/) (Patchstack). For each site with a match, xCloud:

1. Takes a backup of the site (on by default).
2. Updates each vulnerable plugin, theme and WordPress core to the newest version WordPress offers.
3. Checks the site again and reports each finding as `updated`, `resolved` or `needs_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:sites` to preview and submit, and `read:sites` to read the results. See [How to Access the xCloud API](/docs/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](/docs/team-roles-permissions-in-xcloud/).
- For MCP, an AI agent connected to xCloud MCP. See [How to Connect xCloud MCP to Your AI Agent](/docs/how-to-connect-xcloud-mcp-to-ai-agent/).
- Vulnerability Scan turned on for the servers whose sites you want to fix. See [Vulnerability Checker](/docs/vulnerability-checker-in-xcloud/).

## 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.

```bash
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`:

```json
{
  "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.

```bash
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:

```json
"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](/docs/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.

```bash
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:

```text
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](https://app.xcloud.host/api/v1/docs), and authentication and rate limits in the [xCloud API documentation overview](/docs/xcloud-api/).
- See every security issue across your sites and servers in one place with the [Security Center](/docs/find-and-fix-security-issues-with-xcloud-security-center/).

If you run into any issues with the bulk vulnerability fix, feel free to reach out to our [support team](/docs/access-built-in-support-portal-in-xcloud/) for help.
