Install
openclaw skills install @benkalsky/wordpress-api-proProduction-grade WordPress REST API integration for managing posts, pages, media, WooCommerce products, Elementor content, SEO meta, ACF, and JetEngine fields. Use when you need to retrieve, draft, create, or update WordPress content programmatically on sites where the user has provided explicit credentials. For any operation that writes to a live site, get explicit user approval for the target site, post/product IDs, and final action before executing. Prefer drafts first. Run batch operations in dry-run mode first; use --execute only after review. Remote URL media downloads and local file reads are restricted by safety boundaries. Also includes a no-auth Tier-1 site audit (PageSpeed, SSL, security headers, CMS/PHP fingerprint, SEO basics) for cold pre-sale checks, and authenticated plugin/SEO-stack discovery.
openclaw skills install @benkalsky/wordpress-api-proManage WordPress sites through the REST API. Runs as an OpenClaw skill or in Claude Code.
This skill runs the scripts/*.py directly. From the skill directory (~/.claude/skills/wordpress-api-pro/ after bash INSTALL.sh):
WP_URL / WP_USERNAME / WP_APP_PASSWORD, or use config/sites.json for multi-site.requests (python3 -m pip install requests, ideally in a venv). The core post/page/media/WooCommerce/batch scripts use the stdlib only.http://site.local) work — the private/HTTP restriction applies only to --allow-remote-url media downloads, not the WP API base URL.claude-elementor-pro): build page structure with the MCP, then do media uploads, SEO meta, custom fields, and WooCommerce here.config/sites.json, keep it local, untracked, and chmod 600 config/sites.json.--execute only after reviewing the dry-run output.--allow-all only when the user explicitly approved all configured sites.--content-file and media uploads can read only from the current working directory by default. Set WP_ALLOWED_FILE_ROOTS to opt into another safe directory.upload_media.py requires --allow-remote-url or WP_ALLOW_REMOTE_URLS=1, allows HTTPS only, and blocks private/local network hosts.seo_meta.py emits a stderr WARNING when writing a key not in the Rank Math / Yoast allowlist. Set WP_REQUIRE_ALLOWLIST=1 to refuse instead. ACF/JetEngine custom-field keys are unaffected — arbitrary keys are their intended API.create_post.py and update_post.py prompt for confirmation before --status publish when run interactively. Pass --yes / -y to bypass. Non-interactive/agent runs are unchanged.Recommended environment variables:
export WP_URL="https://example.com"
export WP_USERNAME="wp-api-user"
read -rs WP_APP_PASSWORD
export WP_APP_PASSWORD
Application Password setup:
https://your-site.example/wp-admin/profile.php.python3 scripts/get_post.py --post-id 123
python3 scripts/list_posts.py --per-page 10 --status publish
python3 scripts/create_post.py \
--title "Draft title" \
--content "Draft content" \
--status draft
python3 scripts/update_post.py \
--post-id 123 \
--title "Approved title" \
--content "Approved content" \
--status draft
By default the file must be under the current working directory:
python3 scripts/update_post.py \
--post-id 123 \
--content-file ./content/post-123.html \
--status draft
To opt into another safe folder:
export WP_ALLOWED_FILE_ROOTS="/absolute/path/to/approved-content"
python3 scripts/update_post.py --post-id 123 --content-file /absolute/path/to/approved-content/post.html
Copy the template locally:
cp config/sites.example.json config/sites.json
chmod 600 config/sites.json
Use a dedicated user per site and keep app_password values local only.
{
"sites": {
"sample-site": {
"url": "https://example.com",
"username": "wp-api-user",
"app_password": "",
"description": "Sample site; put the real credential only in local config/sites.json"
}
},
"groups": {
"sample": ["sample-site"]
}
}
./wp.sh --list-sites
./wp.sh sample-site get-post --id 123
./wp.sh sample-site update-post --id 123 --status draft
Group operations require an explicit flag:
./wp.sh sample --execute-group update-post --id 123 --status draft
If the group is named all, add --allow-all only after explicit approval:
./wp.sh all --execute-group --allow-all update-post --id 123 --status draft
Batch mode is dry-run unless --execute is present:
python3 scripts/batch_update.py \
--group sample \
--post-ids 123,456 \
--status draft
Apply after review:
python3 scripts/batch_update.py \
--group sample \
--post-ids 123,456 \
--status draft \
--execute
Targeting every site requires explicit opt-in:
python3 scripts/batch_update.py \
--group all \
--allow-all \
--post-ids 123 \
--status draft
Local file upload, restricted to allowed file roots:
python3 scripts/upload_media.py \
--file ./media/image.jpg \
--title "Image title"
Remote URL upload, explicit opt-in and HTTPS-only:
python3 scripts/upload_media.py \
--file https://cdn.example.com/image.jpg \
--allow-remote-url \
--title "Image title"
scripts/detect_plugins.py — detect ACF, Rank Math, Yoast, JetEngine.scripts/acf_fields.py — read/write ACF fields.scripts/seo_meta.py — read/write Rank Math and Yoast SEO metadata.scripts/jetengine_fields.py — read/write JetEngine custom fields.scripts/site_audit.py — no-auth Tier-1 website audit (PageSpeed/SSL/security headers/CMS+PHP/SEO basics). Public probes only; run cold pre-sale.scripts/describe_cpt.py — discover a CPT's rest_base, taxonomies, and field keys (read-only).scripts/seed_content.py — batch-create CPT entries with ACF/Jet fields, taxonomies, and featured images from a JSON dataset. Dry-run by default; pass --execute to write.scripts/elementor_content.py — read/update Elementor _elementor_data.scripts/woo_products.py — manage WooCommerce products.For dynamic sites (JetEngine/ACF listings), populate the entries the listings render:
describe_cpt.py --post-type projects — learn the rest_base, taxonomies, field keys.{post_type, title, content, status, terms, featured_image, acf, jet}).seed_content.py --dataset data.json — review the dry-run plan (no writes, stdlib-only).seed_content.py --dataset data.json --execute — create (drafts by default).Notes: the CPT, taxonomies, and ACF field-groups must already exist (admin-side).
featured_image accepts a media id or a URL/path (URL fetch needs --allow-remote-url).
--execute needs the requests dependency (used by the ACF/Jet writers). Re-running
creates duplicates (no upsert yet).
Before any live mutation:
draft unless the user explicitly approves publish.