WordPress bot detection and email validation
A single-file must-use plugin that screens comments and registrations. No settings page, no dashboard widget, no upsell.
1. Install
Drop the file into wp-content/mu-plugins/ and create the folder if it is absent.
2. Add the file
wp-content/mu-plugins/conure-guard.php
<?php
/**
* Plugin Name: Conure Guard
* Description: Screens comments and registrations against Conure API.
*/
defined('ABSPATH') || exit;
function conure_request($path, $payload) {
$key = defined('CONURE_API_KEY') ? CONURE_API_KEY : getenv('CONURE_API_KEY');
if (empty($key)) {
return null;
}
$response = wp_remote_post('https://uat.conureapi.com' . $path, [
'timeout' => 3,
'headers' => [
'Authorization' => 'Bearer ' . $key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode($payload),
]);
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
return null; // fail open
}
$body = json_decode(wp_remote_retrieve_body($response), true);
return is_array($body) ? $body : null;
}
add_filter('preprocess_comment', function ($comment) {
$verdict = conure_request('/v1/bot-check', [
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
]);
if ($verdict !== null && ! empty($verdict['is_bot'])) {
wp_die(
esc_html__('Your comment was blocked as automated traffic.', 'conure'),
esc_html__('Comment blocked', 'conure'),
['response' => 403, 'back_link' => true]
);
}
return $comment;
});
add_filter('registration_errors', function ($errors, $login, $email) {
$verdict = conure_request('/v1/email-check', ['email' => $email]);
if ($verdict !== null && ! empty($verdict['is_risky'])) {
$errors->add('conure_email', __('Please register with a permanent email address.', 'conure'));
}
return $errors;
}, 10, 3);3. Wire it up
// wp-config.php - above the "That's all, stop editing!" line
define('CONURE_API_KEY', 'your_key_here');4. Verify
Confirm the API answers before you debug your WordPress
wiring. The sample address sits inside a published AWS range, so a correct setup
returns is_bot: true.
curl -X POST https://uat.conureapi.com/v1/bot-check \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ip":"52.1.2.3","user_agent":"curl/8.4.0"}'
import requests
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.post(
"https://uat.conureapi.com/v1/bot-check",
headers=HEADERS,
json={"ip": "52.1.2.3", "user_agent": "curl/8.4.0"},
timeout=5,
)
response.raise_for_status()
print(response.json())
const response = await fetch("https://uat.conureapi.com/v1/bot-check", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({"ip":"52.1.2.3","user_agent":"curl/8.4.0"}),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error("conure: " + response.status);
console.log(await response.json());
$curl = curl_init("https://uat.conureapi.com/v1/bot-check");
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => '{"ip":"52.1.2.3","user_agent":"curl/8.4.0"}',
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_KEY",
"Content-Type: application/json",
],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$verdict = $status === 200 ? json_decode($body, true) : null;
var_dump($verdict);
require "net/http"
require "json"
uri = URI("https://uat.conureapi.com/v1/bot-check")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = { ip: "52.1.2.3", user_agent: "curl/8.4.0" }.to_json
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: uri.scheme == "https",
open_timeout: 5, read_timeout: 5) do |http|
http.request(request)
end
puts JSON.parse(response.body)
Notes
- Must-use plugins load before regular plugins and cannot be deactivated from the admin, which is what you want for a security filter.
- Behind Cloudflare, REMOTE_ADDR is Cloudflare. Use HTTP_CF_CONNECTING_IP instead.