DEPLOY May 8, 2026 12 min read

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…

by Bugi 12 min
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 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.
Note
This guide assumes WordPress is already installed and running. If you need WordPress setup, handle that first.

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
Danger
Never commit .env to version control. Add it to .gitignore immediately. Leaked API keys get scraped within minutes.

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');
1
Upload mu-plugin
Place ai-bot-bridge.php in wp-content/mu-plugins/.
2
Add constants
Set AIBOT_SERVICE_URL and AIBOT_WEBHOOK_SECRET in wp-config.php.
3
Create application password
In WordPress admin → Users → your bot user → Application Passwords. Copy to .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
Warning
Bind the bot to 127.0.0.1 only. It should not be reachable from the public internet — WordPress proxies requests to it internally.

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.

Takeaway

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.

Tip
Log webhook payloads to a file during initial setup. Once verified, switch to structured logging with redacted secrets.

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 .env files work for small deployments.
  • Restrict the bot user’s WordPress role. Create a dedicated user with Author or Contributor role — never Administrator. The edit_posts capability 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?
Yes. Set 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?
The mu-plugin loads network-wide on multisite, so all sites get the REST endpoints. You’ll want to scope webhooks per site by including the blog ID in the payload and routing accordingly in the bot service. Add get_current_blog_id() to the webhook payload array.
How do I handle long-running AI requests without timing out?
For chat responses, 30 seconds (set in 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?
No. Outbound webhooks use '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?
Yes. Change 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?
Use a blue-green approach: deploy the new version to a second systemd service on a different port, test it, then update the Nginx upstream to point to the new port. Or use 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?
Any hook. The example uses 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.
Can I use a local LLM instead of OpenAI?
Yes. Set 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?
The mu-plugin loads network-wide on multisite, so all sites get the REST endpoints. Scope webhooks per site by including the blog ID in the payload and routing accordingly in the bot service.
How do I handle long-running AI requests without timing out?
For chat responses, 30 seconds covers most completions. For longer tasks, 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?
No. Outbound webhooks use blocking=false, so WordPress fires them and continues without waiting. The chat endpoint blocks during bot responses but only affects that visitor, not other page loads.
Can I deploy the bot service on a different server than WordPress?
Yes. Change AIBOT_SERVICE_URL in wp-config.php to the remote URL. Secure the connection with HTTPS and restrict access by IP or mutual TLS.
How do I update the bot without downtime?
Use a blue-green approach with a second systemd service on a different port, or accept the 2-3 second restart window from systemctl restart since webhooks retry on failure.
What WordPress hooks can trigger the bot?
Any hook. Common additions include wp_insert_comment, user_register, woocommerce_order_status_changed, and save_post. Add more by calling aibot_fire_webhook() from any action hook.