Docs API

CLI ingest contract

The token-authenticated POST /api endpoint the CaptainCore CLI posts to, every command it accepts, and the key:value format the remote data collector returns.

The Manager never connects to customer servers. The CaptainCore CLI, a Go binary running on a dedicated server, does that work over SSH and posts the results back to one endpoint:

POST /wp-json/captaincore/v1/api

The route’s permission callback is __return_true, because authentication happens inside the handler with a shared token rather than a WordPress user.

Request shape

The body is a single JSON object. command and token are always present. Everything else depends on the command.

{
  "command": "sync-data",
  "token": "the-cli-token",
  "site_id": 135,
  "data": { "environment_id": 3365, "core": "6.9.1" }
}

The CLI’s client (apiclient/client.go) builds this by taking a payload map, setting payload["command"] and payload["token"], then posting it as application/json with a 30 second timeout. There are no headers beyond the content type, so the token is the whole gate.

Token validation

if ( empty( $post ) || empty( $post->token ) || ! is_string( $post->token )
     || ! hash_equals( (string) captaincore_get_cli_token(), $post->token ) ) {
    return new WP_Error( 'token_invalid', 'Invalid Token', [ 'status' => 404 ] );
}

Three things are load-bearing here. The comparison is constant time. The token must be a string, because a JSON true would type-juggle past a loose !=. And a failure returns 404 rather than 403, so the endpoint does not confirm it exists to an unauthenticated prober.

The token comes from captaincore_get_cli_token(). If the CAPTAINCORE_CLI_TOKEN constant is defined, that wins. Otherwise the value is read from the captaincore_cli_token option, and generated as a 64 character random string on first use.

command gets the same string check, for the same reason. A JSON true would satisfy every loose comparison in the branch ladder at once and fire every handler on one request.

Site and environment resolution

After the token check:

$current_site = CaptainCore\Sites::get( $site_id );
if ( empty( $current_site ) && $site_id != "" && $command !== "default-get"
     && $command !== "configuration-get" && $command !== "providers-list-raw" ) {
    return new WP_Error( 'command_invalid', "Invalid Command for $site_id", [ 'status' => 404 ] );
}

So an unknown site_id is rejected, except for the three read-only lookups that legitimately carry no site. Then:

  • $site_name and $domain_name come from the site row.
  • $environment_id is resolved with ( new Site( $site_id ) )->fetch_environment_id( $environment ), matching the environment name.

Most write commands carry data.environment_id directly and use that instead, which is why they work even when environment is absent.

Commands

Data sync

Command What it stores
sync-data The main sync. Updates the environment row with plugins, themes, core, home_url, users, database credentials, checksum flag, subsite count, php_memory and token. Merges incoming details over existing details so server-side flags survive. Recomputes the audit coverage summary, refreshes screenshot_base from the latest capture, rebuilds the site’s environments cache, and updates the site name from home_url on Production. Fires checksum, default-role and open-registration alert emails once per condition, tracked by flags in details.
update-environment Same merge behaviour, without the alerting or the site-name rewrite.
update-site Updates a site row, merging details so flags such as removed are preserved.
update-fathom Writes details.fathom on one environment.
usage-update Updates visits and storage. A visits of 0 or empty keeps the existing value rather than overwriting good data with a failed API read, then recalculates site details.
token Stores token_key on the site row.

Security and monitoring

Command What it stores
session-snapshot Inserts a row into captaincore_session_snapshots: user and session counts, admin IP counts, injected capability count, super admin count, plus the full collector JSON in payload. Runs delta detection against the previous snapshot for that environment and stores the anomalies and max severity. Anomalies also write an session_anomaly activity log row. Email is deliberately not sent here; wp captaincore session-alerts batches them hourly.
malware-alert Sends a malware alert email from data.findings.
capture-alert Sends a visual capture injection alert.
monitor-notify Sends an uptime alert with data.subject and data.content.
core-update-run Stores a fleet core-update run: a parent row in captaincore_core_update_runs plus per-site rows in captaincore_core_update_results. Returns run_id and inserted.

Captures, snapshots and backups

Command What it stores
new-capture Inserts a captaincore_captures row. Builds the per-page image list from capture_pages and captured_pages, keeping the previous image for pages that were not re-captured this run. Converts the epoch created_at to a MySQL datetime, then points the environment’s screenshot_base at the new set and refreshes the environments cache.
snapshot-add Inserts or updates a captaincore_snapshots row keyed on snapshot_id, then emails the download link.
backup-download-notify Sends the “your backup is ready” email with the file count, timestamp and download URL.

Deploy notifications

copy, production-to-staging and staging-to-production each send a completion email when email is present. They store nothing.

Read-only lookups

Command Returns
site-get-raw The raw site record for site_id
account-get-raw The raw account record for account_id
providers-list-raw Every provider row
configuration-get The configuration object
default-get Global defaults

Destructive

site-delete deletes the site row for post.site_id.

The bulk connect endpoint

POST /wp-json/captaincore/v1/cli/connect is the CLI’s initial handshake and periodic resync. Its permission callback accepts either an administrator application password or the CLI token in the body, compared the same constant-time way.

It returns, in one response: the CLI token, the API URL, the dashboard URL, configurations, defaults, and bulk selects from captaincore_sites, captaincore_environments, captaincore_accounts, captaincore_providers, captaincore_domains, captaincore_account_site, captaincore_account_domain and captaincore_account_user. Sites, environments and accounts select only the columns the CLI needs, to keep the response from exhausting memory on a large fleet.

The fetch-site-data format

sync-data gets its content from a bash script that runs on the remote site, lib/remote-scripts/fetch-site-data. It returns key:value pairs, one per line.

plugins:[{"name":"akismet","title":"Akismet","status":"active","version":"5.3"}]
core:6.9.1
home_url:https://example.com
php_memory:256M

Split on the first colon only. Values routinely contain JSON, and JSON contains colons. The CLI does this with strings.Cut:

func parseSiteData(output string) map[string]string {
    data := map[string]string{}
    for _, line := range strings.Split(output, "\n") {
        key, value, found := strings.Cut(line, ":")
        if found {
            data[key] = value
        }
    }
    return data
}

If wp-config.php is missing the script prints WordPress not found and exits. The CLI recognises that exact string and posts a minimal sync-data with token set to basic, rather than treating it as a failure.

Keys the script emits

Key Destination
plugins, themes Environment columns. Both must parse as JSON or the CLI aborts with “Response not valid”
core, home_url, users, subsite_count, php_memory, token Environment columns
database_name, database_username, database_password Environment columns
core_verify_checksums Environment column, 0 means the checksum pass failed
component_hashes Not stored directly. Merged as a hash field into each matching plugin and theme object. Keys prefixed mu: are also merged into mu_plugins and into the plugins array, because WP-CLI lists must-use plugins there
mu_plugins Stored in details.mu_plugins, with hashes merged in
default_role, registration, restic_cache, php_version, db_size Stored as plain strings in details
core_checksum_details, plugin_checksum_details, security_log, error_logs, mu_plugin_files, core_file_hashes, loose_file_hashes, capture_plugin_pages Parsed as JSON before being stored in details. A value that fails to parse is skipped
session_signal Not part of the sync-data payload. Posted separately as a session-snapshot command, best effort, so a failure there cannot disrupt the sync

The script installs the captaincore-helper must-use plugin if it is missing, then runs wp plugin list, wp theme list, wp core version, wp user list and wp core verify-checksums with --skip-themes --skip-plugins --skip-packages so a broken site still reports.

Checksum output is parsed into three categories. Modified or extra files set the status to fail and core_verify_checksums to 0. Missing files alone are a warning.

Writing your own ingest client

If you are replacing or extending the CLI, the contract is small:

  1. Read the token from GET /me context, POST /cli/connect as an administrator, or the captaincore_cli_token option.
  2. Post JSON to /wp-json/captaincore/v1/api with command, token, and whatever the command needs.
  3. Send site_id for anything site-scoped, and data.environment_id for anything environment-scoped.
  4. Treat a 404 with code token_invalid as an authentication failure, and a 404 with command_invalid as an unknown site.

Every command returns a JSON object with a response string describing what happened, plus command-specific fields.