How to Run and Track Site Commands with the xCloud Public API

Updated October 30, 2026 · 7 min read

Summarize with:
Share:

You can run a one-off command on any xCloud site from a script and follow it to the end: send the command with POST /api/v1/sites/{site}/commands, then read its status, exit code and output with GET /api/v1/sites/{site}/commands/{task}. The command runs as the site’s user, never as root. It replaces the old trick of adding a temporary cron job just to run something once, such as wp cache flush after a deploy.

Site commands are new in xCloud v2.9.7.4. They are available through the Public API only.

What you need first

All four are checked on every request:

Requirement Where to set it
An API token with the commands:run permission Settings → Developers → API Tokens (Step 1 below)
Two-factor authentication turned on for your account See Two-Factor Authentication
The Team Admin, Server Admin or Site Admin role in the site’s team Your team owner sets roles
Permission to use the site’s Commands page The same permission the dashboard uses; if you see Commands in the site menu, you have it

Full Access does not include commands:run. Running commands is opt-in, so a token made for reading servers or deploying sites cannot run commands unless you ticked this box. Tokens used by the in-app AI Assistant are always refused.

Step 1: Create a token with commands:run

Go to Settings → Developers → API Tokens and click Create New Token. In Create A New API Token, enter a Token Name, untick Full Access (all API permissions), then tick commands:run. Add any other permissions your script needs, and click Create Token.

Tick ‘commands:run’ with ‘Full Access’ unticked in the Create A New API Token window

Expected result: the new token is shown once. Copy it and store it somewhere safe. The token list shows commands:run among its permissions.

Leave at least one permission unticked. If you tick every box, the window switches back to Full Access, which does not grant commands:run. A token with only commands:run is the safest choice for a script that just runs commands. For more on tokens, see How to Access the xCloud API.

Step 2: Find the site’s UUID

Commands are sent to one site, named by its UUID. List your sites and copy the uuid of the one you want:

export XCLOUD_TOKEN='YOUR_API_TOKEN'

curl 'https://app.xcloud.host/api/v1/sites' \
  --header "Authorization: Bearer $XCLOUD_TOKEN" \
  --header 'Accept: application/json'

Expected result: a 200 response with your sites. Store the UUID as SITE_UUID. To work in another team the token is granted to, add the X-Team-Id header to every request in this guide; see Multi-Team API Tokens and MCP Access.

Step 3: Send the command

Send the command in a JSON body with a unique Idempotency-Key header. The header is required: it makes the request safe to retry, because a repeated request never runs the command twice. Make one random key per command, for example with uuidgen, and keep it in a variable. If a request times out, retry it with the same key so the command is not queued twice. Make a new key only for a new command.

export SITE_UUID='3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f'
export IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST "https://app.xcloud.host/api/v1/sites/$SITE_UUID/commands" \
  --header "Authorization: Bearer $XCLOUD_TOKEN" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data '{"command": "wp cache flush"}'

Expected result: 202 Command queued, with the ID you use to follow the command and the URL to poll. Example response (the IDs are placeholders):

{
  "success": true,
  "message": "Command queued",
  "data": {
    "task_uuid": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    "status_url": "https://app.xcloud.host/api/v1/sites/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/commands/7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b"
  }
}

Copy data.task_uuid from the response and store it for the next step:

export TASK_UUID='7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b'

You can also call data.status_url directly; it is the same address.

The body takes only command, at most 4,096 bytes. You cannot choose the user: a user or site_id field is rejected with 422, and the command always runs as the site’s user in the background.

Step 4: Poll for the result

Call status_url until status is no longer queued or running:

curl "https://app.xcloud.host/api/v1/sites/$SITE_UUID/commands/$TASK_UUID" \
  --header "Authorization: Bearer $XCLOUD_TOKEN" \
  --header 'Accept: application/json'

Expected result: each poll returns 200. When status is finished, failed, timeout or killed, the command has ended. Example response (the IDs, times and output are placeholders; yours show your command’s real output):

{
  "success": true,
  "message": "Success",
  "data": {
    "status": "finished",
    "exit_code": 0,
    "output": "Success: The cache was flushed.\n",
    "truncated": false,
    "started_at": "2026-10-30T12:10:04+00:00",
    "finished_at": "2026-10-30T12:10:06+00:00"
  }
}
Field Meaning
status queued, running, then one of finished, failed, timeout or killed
exit_code The command’s exit code, or null while it has none
output The command’s output. Long output keeps only the last 60,000 bytes
truncated true when the output was longer than 60,000 bytes and the start was cut
started_at, finished_at When the command started and ended, or null

Check exit_code as well as status: a command that ran but returned an error exits with a non-zero code. Only the person who sent a command can read it, and only while they still have access to the site; anyone else gets 404. Responses are never cached.

A small polling loop with jq:

until STATUS=$(curl -sf "https://app.xcloud.host/api/v1/sites/$SITE_UUID/commands/$TASK_UUID" \
    --header "Authorization: Bearer $XCLOUD_TOKEN" --header 'Accept: application/json' \
    | jq -r '.data.status'); [ -n "$STATUS" ] && [ "$STATUS" != "null" ] \
    && [ "$STATUS" != "queued" ] && [ "$STATUS" != "running" ]; do
  sleep 5
done
echo "Command ended: $STATUS"

If a poll fails, for example with 429 or a network error, the loop waits and polls again instead of stopping.

Limits

Limit Value
Commands sent (POST) 30 per hour per token, and 120 per hour per team
Status checks (GET) 120 per minute per token, and 120 per minute per team, counted apart from POST
Command length 4,096 bytes
Output returned The last 60,000 bytes

Going over a limit returns 429. Wait and try again; for status checks, a few seconds between polls keeps you well inside the limit.

xCloud keeps a record of every command you send and every status check, and of requests refused because the token lacks commands:run or two-factor authentication is off: who sent it, with which token, for which site, and when. The record does not hold the command text or its output.

If something goes wrong

Response What it means and what to do
403 command_ability_required The token has no commands:run, or it is a Full Access or AI Assistant token. Create a token as in Step 1
403 two_factor_required Turn on two-factor authentication for your account, then retry with the same token
403 (other) or 404 on POST Your role or permissions do not cover this site, or the site is not in the token’s team. Check the role, the Commands permission and X-Team-Id
404 on GET The command was sent by someone else, belongs to another site, or you lost access to the site
409 A request with the same Idempotency-Key is still being accepted. Wait a moment and retry it unchanged
422 The command is missing or longer than 4,096 bytes, the Idempotency-Key is missing, the key was used for a different command, or the site runs as root (command_runner_root_site_unsupported)
429 You hit a limit from the table above. Wait and retry

To run a command by hand instead, use the site’s Commands page in the dashboard; it works as before. See How to Run Commands in Your Site and Server. For every other endpoint, see the xCloud API reference.

Frequently asked questions

Why does my Full Access token get command_ability_required?

Full Access does not include commands:run. Running commands is opt-in: create a token with Full Access unticked and commands:run ticked. Tokens used by the in-app AI Assistant are refused too.

Can a command run as root?

No. Every command runs as the site’s own user. A site whose user is root is refused with command_runner_root_site_unsupported.

What happens if my script sends the same request twice?

The command runs once. A repeat with the same Idempotency-Key and the same command returns the first answer. The same key with a different command returns 422, and a repeat that arrives while the first is still being accepted returns 409.

Can I run site commands through xCloud MCP?

Not yet. In this release only the Public API offers these two operations. MCP clients and the in-app AI Assistant cannot run them, and the Commands page in the dashboard works as before.

If you run into any issues, feel free to reach out to our support team.