Claude API WordPress Plugin Development Guide
If you want to add Claude-powered features to WordPress — content suggestions, a chat widget, auto-generated excerpts, support ticket triage — you need a plugin that talks to the Claude API from PHP, handles authentication safely, and renders output somewhere useful in the admin or frontend. This guide walks through the actual plugin structure: file layout, the HTTP call, nonce and capability checks, storing keys securely, and exposing the feature via a shortcode or Gutenberg block.
The short answer to "how do I connect Claude to WordPress" is: build a small plugin that makes a server-side HTTP request (via wp_remote_post), never expose your API key in JavaScript, and cache or rate-limit responses so you don't burn through usage on every page load. Everything below expands on that.
Plugin file structure
A minimal plugin needs three things: a bootstrap file, a settings page for the API key, and the request handler.
claude-wp-plugin/
├── claude-wp-plugin.php
├── includes/
│ ├── class-claude-client.php
│ ├── class-claude-admin.php
│ └── class-claude-shortcode.php
└── assets/
└── admin.js
The bootstrap file just registers hooks:
<?php
/**
* Plugin Name: Claude Assistant
* Description: Adds Claude-powered text generation to WordPress.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) exit;
require_once plugin_dir_path(__FILE__) . 'includes/class-claude-client.php';
require_once plugin_dir_path(__FILE__) . 'includes/class-claude-admin.php';
require_once plugin_dir_path(__FILE__) . 'includes/class-claude-shortcode.php';
new Claude_Admin();
new Claude_Shortcode();
Storing the API key securely
Never hardcode the key or put it in a frontend JS file. Store it with register_setting and read it server-side only.
class Claude_Admin {
public function __construct() {
add_action('admin_menu', [$this, 'add_menu']);
add_action('admin_init', [$this, 'register_settings']);
}
public function add_menu() {
add_options_page('Claude Settings', 'Claude', 'manage_options', 'claude-settings', [$this, 'render']);
}
public function register_settings() {
register_setting('claude_options', 'claude_api_key', [
'sanitize_callback' => 'sanitize_text_field',
]);
}
public function render() {
?>
<div class="wrap">
<h1>Claude API Settings</h1>
<form method="post" action="options.php">
<?php
settings_fields('claude_options');
do_settings_sections('claude_options');
?>
<input type="password" name="claude_api_key"
value="<?php echo esc_attr(get_option('claude_api_key')); ?>" style="width:400px" />
<?php submit_button(); ?>
</form>
</div>
<?php
}
}
This is also where SubToAPI is worth considering if your plugin targets site owners who already pay for Claude via a personal or Pro account but don't want to manage raw Anthropic billing or key rotation across multiple sites. Instead of asking each user to generate and paste an Anthropic API key, you can point them to /signup to get a sub_live_... key from SubToAPI, which wraps their existing Claude access behind a standard HTTPS API with per-key usage visibility — useful if your plugin is distributed to clients who aren't comfortable managing API billing themselves.
Making the request
Use wp_remote_post, not curl directly — it respects WordPress HTTP filters, proxies, and timeouts configured by the host.
class Claude_Client {
private $endpoint = 'https://api.anthropic.com/v1/messages';
public function generate($prompt, $max_tokens = 500) {
$api_key = get_option('claude_api_key');
if (!$api_key) {
return new WP_Error('no_key', 'Claude API key is not configured.');
}
$response = wp_remote_post($this->endpoint, [
'headers' => [
'x-api-key' => $api_key,
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
],
'body' => wp_json_encode([
'model' => 'claude-sonnet-4-5',
'max_tokens' => $max_tokens,
'messages' => [['role' => 'user', 'content' => $prompt]],
]),
'timeout' => 30,
]);
if (is_wp_error($response)) {
return $response;
}
$body = json_decode(wp_remote_retrieve_body($response), true);
return $body['content'][0]['text'] ?? '';
}
}
If you'd rather not manage raw Anthropic auth, key storage, and version headers yourself, swap the endpoint for SubToAPI's and use a Bearer token instead — the request shape stays the same. See /docs/messages for the exact payload format and /docs/quickstart for setup.
Exposing it via a shortcode
A shortcode is the fastest way to let editors trigger generation from the block editor without writing a custom block.
class Claude_Shortcode {
public function __construct() {
add_shortcode('claude_generate', [$this, 'render']);
}
public function render($atts) {
$atts = shortcode_atts(['prompt' => ''], $atts);
if (empty($atts['prompt'])) return '';
$cache_key = 'claude_' . md5($atts['prompt']);
$cached = get_transient($cache_key);
if ($cached !== false) return esc_html($cached);
$client = new Claude_Client();
$result = $client->generate($atts['prompt']);
if (is_wp_error($result)) {
return '<em>Claude request failed.</em>';
}
set_transient($cache_key, $result, HOUR_IN_SECONDS);
return esc_html($result);
}
}
Caching with set_transient matters here — without it, every page view re-triggers a Claude call, which is slow and costly. For content that doesn't change per-visitor, cache aggressively (hours or days). For anything user-specific, key the cache on user ID and keep the TTL short.
Handling streaming and AJAX requests
If you want a live "typing" effect in the admin (e.g., an excerpt generator in the post editor sidebar), don't stream raw Server-Sent Events through PHP — it's fragile behind WordPress's output buffering and caching plugins. Instead, make the AJAX call non-streaming and show a loading state client-side:
add_action('wp_ajax_claude_generate', function () {
check_ajax_referer('claude_nonce', 'nonce');
if (!current_user_can('edit_posts')) wp_die('Unauthorized', 403);
$prompt = sanitize_textarea_field($_POST['prompt'] ?? '');
$client = new Claude_Client();
$result = $client->generate($prompt);
wp_send_json_success(['text' => is_wp_error($result) ? '' : $result]);
});
Always include check_ajax_referer and a capability check — without them, any logged-in (or logged-out, if you forget wp_ajax_nopriv_) visitor could trigger paid API calls through your endpoint. If you do want true token-by-token streaming in a custom admin UI, it's easier to proxy through a lightweight external service that already handles SSE correctly — see /docs/streaming for the request/response format.
Rate limiting and cost control
A plugin running on a public site needs guardrails:
- Cap requests per user per hour using a transient counter.
- Set a hard
max_tokensceiling in your client class, not just in the UI. - Log failures and token usage to a custom table so you can see cost trends before they surprise you.
- If multiple editors or client sites share one plugin install, give each a separate API key so usage is attributable — SubToAPI's dashboard shows per-key usage if you're distributing the plugin across several WordPress installs under one account.
Submitting to the WordPress.org plugin directory
If you plan to distribute publicly, the review team will reject plugins that call external APIs without clear disclosure. Add a readme.txt section stating what data is sent (the prompt text) and to which service, and never bundle a default API key — require every installer to supply their own.
questions
Do I need a special WordPress library to call Claude? No. wp_remote_post is sufficient for both streaming-disabled and standard requests; you don't need the Anthropic SDK inside a PHP plugin.
Can I let site visitors trigger Claude requests directly? Only with strict rate limiting and nonce/capability checks. Unrestricted public-facing AJAX endpoints are the most common way plugins rack up unexpected API costs.
Should I store the API key in wp_options or an environment variable? wp_options with register_setting is standard and fine for most sites; for multisite or agency deployments, an environment variable read via getenv() avoids key duplication across database exports.