How to Deploy an AI Bot on WordPress: Automation Setup Guide
TL;DR Deploy an AI bot on WordPress using the REST API, a lightweight mu-plugin, and webhook-based automation. No third-party SaaS chatbot platform required — self-hosted, full control over data and…
- Deploy an AI bot on WordPress using the REST API, a lightweight mu-plugin, and webhook-based automation.
- No third-party SaaS chatbot platform required — self-hosted, full control over data and prompts.
- Works with any OpenAI-compatible API (OpenAI, Anthropic via proxy, local models via Ollama).
Overview
This guide walks through deploying a self-hosted AI bot on WordPress that handles content automation, visitor chat, and scheduled tasks. The stack: a custom mu-plugin for the WordPress side, an external bot service (Node.js or Python), and webhook glue between them.
Total setup time: 30–60 minutes for a basic deployment. You need SSH access to your server, a WordPress installation running PHP 8.0+, and an API key for your chosen LLM provider.
The architecture keeps the AI processing outside WordPress itself — the bot runs as a separate service, communicates via REST endpoints, and triggers WordPress actions through authenticated API calls.
What You’ll Need
- Server: VPS with 1 GB+ RAM (bot service + WordPress). A $6/mo DigitalOcean droplet or equivalent works.
- WordPress: 6.0+ with REST API enabled (default). PHP 8.0+.
- Runtime: Node.js 18+ or Python 3.10+ for the bot service.
- LLM API key: OpenAI, Anthropic, or a local model endpoint (Ollama).
- Domain: With SSL configured (Let’s Encrypt is fine). Webhooks require HTTPS.
- SSH access: Root or sudo-capable user.
Step 1: Prepare the Bot Service
Set up the external bot service that will process AI requests and communicate with WordPress.
Option A — Node.js bot:
mkdir ~/wp-ai-bot && cd ~/wp-ai-bot
npm init -y
npm install express openai dotenv node-cron
Create the entry point:
// index.js
import express from 'express';
import OpenAI from 'openai';
import cron from 'node-cron';
import 'dotenv/config';
const app = express();
app.use(express.json());
const ai = new OpenAI({
apiKey: process.env.LLM_API_KEY,
baseURL: process.env.LLM_BASE_URL || 'https://api.openai.com/v1',
});
// Webhook endpoint — WordPress calls this
app.post('/webhook', async (req, res) => {
const { action, payload, secret } = req.body;
if (secret !== process.env.WEBHOOK_SECRET) {
return res.status(401).json({ error: 'unauthorized' });
}
// Route to handler based on action
const result = await handleAction(action, payload);
res.json(result);
});
// Health check
app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.listen(process.env.BOT_PORT || 3100);
Option B — Python bot:
mkdir ~/wp-ai-bot && cd ~/wp-ai-bot
python3 -m venv venv && source venv/bin/activate
pip install flask openai python-dotenv apscheduler gunicorn
# app.py
from flask import Flask, request, jsonify
from openai import OpenAI
import os
app = Flask(__name__)
client = OpenAI(
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
)
@app.post("/webhook")
def webhook():
data = request.json
if data.get("secret") != os.getenv("WEBHOOK_SECRET"):
return jsonify(error="unauthorized"), 401
result = handle_action(data["action"], data.get("payload", {}))
return jsonify(result)
@app.get("/health")
def health():
return jsonify(status="ok")
Create the .env file:
LLM_API_KEY=sk-your-key-here
LLM_BASE_URL=https://api.openai.com/v1
WEBHOOK_SECRET=generate-a-strong-random-string
BOT_PORT=3100
WP_URL=https://yoursite.com
WP_APP_USER=bot
WP_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
Step 2: Create the WordPress mu-plugin
The mu-plugin registers custom REST endpoints and fires outbound webhooks to your bot service. mu-plugins load automatically — no activation step.
<?php
/**
* Plugin Name: AI Bot Bridge
* Description: REST endpoints and webhook dispatcher for external AI bot.
*/
defined('ABSPATH') || exit;
// Register REST routes
add_action('rest_api_init', function () {
register_rest_route('ai-bot/v1', '/content', [
'methods' => 'POST',
'callback' => 'aibot_create_content',
'permission_callback' => function () {
return current_user_can('edit_posts');
},
]);
register_rest_route('ai-bot/v1', '/chat', [
'methods' => 'POST',
'callback' => 'aibot_proxy_chat',
'permission_callback' => '__return_true',
]);
});
function aibot_create_content(WP_REST_Request $request) {
$params = $request->get_json_params();
$title = sanitize_text_field($params['title'] ?? '');
$content = wp_kses_post($params['content'] ?? '');
$status = in_array($params['status'] ?? '', ['draft', 'publish'], true)
? $params['status'] : 'draft';
$post_id = wp_insert_post([
'post_title' => $title,
'post_content' => $content,
'post_status' => $status,
'post_type' => 'post',
]);
if (is_wp_error($post_id)) {
return new WP_REST_Response(['error' => $post_id->get_error_message()], 500);
}
return new WP_REST_Response(['post_id' => $post_id], 201);
}
function aibot_proxy_chat(WP_REST_Request $request) {
$message = sanitize_text_field($request->get_json_params()['message'] ?? '');
if (empty($message)) {
return new WP_REST_Response(['error' => 'empty message'], 400);
}
$bot_url = defined('AIBOT_SERVICE_URL')
? AIBOT_SERVICE_URL : 'http://127.0.0.1:3100';
$response = wp_remote_post($bot_url . '/webhook', [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'action' => 'chat',
'payload' => ['message' => $message],
'secret' => defined('AIBOT_WEBHOOK_SECRET') ? AIBOT_WEBHOOK_SECRET : '',
]),
'timeout' => 30,
]);
if (is_wp_error($response)) {
return new WP_REST_Response(['error' => 'bot unreachable'], 502);
}
return new WP_REST_Response(
json_decode(wp_remote_retrieve_body($response), true)
);
}
// Fire webhook on post publish
add_action('transition_post_status', function ($new, $old, $post) {
if ($new !== 'publish' || $old === 'publish') return;
aibot_fire_webhook('post_published', [
'post_id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink($post->ID),
]);
}, 10, 3);
function aibot_fire_webhook(string $action, array $payload): void {
$url = defined('AIBOT_SERVICE_URL')
? AIBOT_SERVICE_URL . '/webhook' : '';
if (empty($url)) return;
wp_remote_post($url, [
'headers' => ['Content-Type' => 'application/json'],
'body' => wp_json_encode([
'action' => $action,
'payload' => $payload,
'secret' => defined('AIBOT_WEBHOOK_SECRET') ? AIBOT_WEBHOOK_SECRET : '',
]),
'blocking' => false,
]);
}
Drop this file into wp-content/mu-plugins/ai-bot-bridge.php. Then add constants to wp-config.php:
define('AIBOT_SERVICE_URL', 'http://127.0.0.1:3100');
define('AIBOT_WEBHOOK_SECRET', 'same-strong-random-string-from-env');
Step 3: Deploy and Run the Bot Service
Use systemd to keep the bot running. Create a service file:
sudo tee /etc/systemd/system/wp-ai-bot.service << 'EOF'
[Unit]
Description=WordPress AI Bot Service
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/home/deploy/wp-ai-bot
EnvironmentFile=/home/deploy/wp-ai-bot/.env
ExecStart=/usr/bin/node index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
For Python, replace ExecStart with:
ExecStart=/home/deploy/wp-ai-bot/venv/bin/gunicorn -w 2 -b 127.0.0.1:3100 app:app
Start the service:
sudo systemctl daemon-reload
sudo systemctl enable wp-ai-bot
sudo systemctl start wp-ai-bot
If your bot service runs on a separate server from WordPress, use an Nginx reverse proxy with mutual TLS or IP allowlisting instead of exposing the port directly.
Step 4: Verify the Deployment
Run these checks in order.
Health check:
~/wp-ai-bot
$ curl http://127.0.0.1:3100/health {"status":"ok"} $ systemctl status wp-ai-bot ● wp-ai-bot.service - WordPress AI Bot Service Active: active (running)
WordPress REST endpoint test:
# Test the chat endpoint
curl -X POST https://yoursite.com/wp-json/ai-bot/v1/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello, are you working?"}'
# Test authenticated content creation
curl -X POST https://yoursite.com/wp-json/ai-bot/v1/content \
-u "bot:xxxx-xxxx-xxxx-xxxx" \
-H "Content-Type: application/json" \
-d '{"title": "Test Post", "content": "<p>Automated.</p>", "status": "draft"}'
Webhook test — publish a post in WordPress admin and check the bot logs:
journalctl -u wp-ai-bot -f
You should see the post_published webhook arrive with the post ID and title.
Step 5: Add Automation Workflows
With the bridge in place, wire up common automation patterns.
Scheduled content generation — add a cron job to the bot service that generates draft posts daily:
// In index.js (Node.js)
cron.schedule('0 8 * * *', async () => {
const completion = await ai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Generate a blog post title and outline about...' }],
});
await fetch(`${process.env.WP_URL}/wp-json/ai-bot/v1/content`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + btoa(`${process.env.WP_APP_USER}:${process.env.WP_APP_PASSWORD}`),
},
body: JSON.stringify({
title: parsed.title,
content: parsed.content,
status: 'draft',
}),
});
});
Comment moderation — add a WordPress hook that sends new comments to the bot for sentiment analysis before approval. Social media triggers — fire a webhook when posts publish, and have the bot generate social snippets via the LLM.
The mu-plugin + external bot pattern separates concerns cleanly: WordPress handles content, the bot handles AI — neither blocks the other.
Troubleshooting
Bot returns 502 / “bot unreachable”
The bot service is down or bound to the wrong interface. Check systemctl status wp-ai-bot and confirm it’s listening on 127.0.0.1:3100. If WordPress is in Docker, 127.0.0.1 won’t resolve to the host — use host.docker.internal or the Docker bridge IP instead.
REST API returns 401 on content creation
Application passwords require HTTPS. If you’re testing over HTTP locally, WordPress silently blocks application password auth. Either use HTTPS or add this filter for local dev only:
add_filter('wp_is_application_passwords_available', '__return_true');
Remove it before deploying to production.
Webhook secret mismatch
The AIBOT_WEBHOOK_SECRET constant in wp-config.php must exactly match WEBHOOK_SECRET in the bot’s .env. Trailing whitespace and encoding mismatches are common. Run echo -n "$WEBHOOK_SECRET" | xxd | head on both sides to compare byte-for-byte.
Security Considerations
A few non-negotiable items for production:
- Rate-limit the chat endpoint. Without limits, anyone can proxy expensive LLM calls through your site. Use a WordPress plugin like WP Rate Limit or add rate limiting in Nginx.
- Validate and sanitize all bot output before inserting into WordPress. The mu-plugin above uses
wp_kses_post(), which strips dangerous HTML. Do not bypass this. - Rotate the webhook secret periodically. Store it in a secrets manager if available; bare
.envfiles work for small deployments. - Restrict the bot user’s WordPress role. Create a dedicated user with Author or Contributor role — never Administrator. The
edit_postscapability check in the mu-plugin enforces this. - Monitor LLM API spend. Set billing alerts on your provider account. A runaway cron job can burn through hundreds of dollars in API credits overnight.
FAQ
Can I use a local LLM instead of OpenAI?
LLM_BASE_URL in your .env to your local endpoint (e.g., http://localhost:11434/v1 for Ollama). Any OpenAI-compatible API works — the bot service just forwards requests. You need enough RAM/VRAM on the server to run the model alongside WordPress.Does this work with WordPress multisite?
get_current_blog_id() to the webhook payload array.How do I handle long-running AI requests without timing out?
wp_remote_post timeout) covers most completions. For longer tasks like content generation, use async processing: the webhook returns immediately with a job ID, the bot processes in the background, then calls back to WordPress with the result via the REST API.Will this slow down my WordPress site?
'blocking' => false, so WordPress fires them and moves on without waiting. The chat endpoint does block while waiting for the bot response, but that only affects the visitor using the chat — not other page loads.Can I deploy the bot service on a different server than WordPress?
AIBOT_SERVICE_URL in wp-config.php to the remote bot’s URL. Secure the connection with HTTPS and restrict access by IP or mutual TLS. The bot also needs network access back to WordPress for authenticated API calls.How do I update the bot without downtime?
systemctl restart — the 2-3 second restart window is acceptable for most WordPress sites since webhooks retry on failure.What WordPress hooks can trigger the bot?
transition_post_status, but common additions include wp_insert_comment (new comments), user_register (new signups), woocommerce_order_status_changed (orders), and save_post (any post save). Add more by calling aibot_fire_webhook() from any action hook.