REST API
The plugin ships a small REST API under the namespace /wkcft/v1. Use it for:
- Verifying Turnstile tokens from a headless / SPA frontend
- Reading analytics programmatically
- Reading or writing conditional rules and Design Studio settings
Base URL
https://yoursite.com/wp-json/wkcft/v1
Authentication
Two auth tiers:
| Endpoint | Auth | Rate Limit |
|---|---|---|
POST /verify | Public | 10 requests / 60 sec per IP |
GET /stats | manage_woocommerce capability | — |
GET /conditions | manage_woocommerce | — |
POST /conditions | manage_woocommerce | — |
GET /design-studio | manage_woocommerce | — |
POST /design-studio | manage_woocommerce | — |
Protected endpoints use standard WordPress REST authentication — Application Passwords, cookie auth for admin UI, OAuth via a plugin like JWT Auth.
POST /verify — Verify a Token
Use this from your headless frontend or SPA to verify a Turnstile token on your own server-to-server.
Request
POST /wp-json/wkcft/v1/verify
Content-Type: application/json
{
"token": "0x4AAA..._LONG_CLOUDFLARE_TOKEN_..."
}
Optional fields:
site_key— Override the stored site key (useful for multi-tenant SaaS)secret_key— Override the stored secret key
Response
{
"success": true,
"error-codes": []
}
Or on failure:
{
"success": false,
"error-codes": ["invalid-input-response"]
}
Rate Limit
- Window: 60 seconds
- Limit: 10 requests per IP
- Exceeded: HTTP 429
Example with fetch()
const response = await fetch('/wp-json/wkcft/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: turnstileToken })
});
const result = await response.json();
if (result.success) {
// Proceed with form submission
} else {
alert('CAPTCHA failed: ' + result['error-codes'].join(', '));
}
Example with curl
curl -X POST https://yoursite.com/wp-json/wkcft/v1/verify \
-H "Content-Type: application/json" \
-d '{"token": "YOUR_TOKEN"}'
GET /stats — Analytics Data
Same data as the Analytics Dashboard, in JSON.
Request
GET /wp-json/wkcft/v1/stats?range=30
Query params:
range(optional) — days to include. Must be one of7,30, or90. Default30. Any other value returns HTTP 400 with coderest_invalid_param.
Response
{
"summary": {
"total": 12500,
"passed": 11200,
"failed": 1300,
"pass_rate": 89.6
},
"top_forms": [
{ "form": "checkout", "total": 5000, "passed": 4700, "failed": 300, "pass_rate": 94.0 },
{ "form": "login", "total": 3000, "passed": 2800, "failed": 200, "pass_rate": 93.3 }
],
"top_errors": {
"invalid-input-response": 800,
"timeout-or-duplicate": 500
},
"range": 30
}
summary— 4 counters, aggregated across the window.top_forms— top 10 forms by volume, each with per-form pass rate.top_errors— map of Cloudflare error code → occurrences.range— echo of the resolved window.
Example with WP REST nonce
const response = await fetch('/wp-json/wkcft/v1/stats?range=7', {
headers: { 'X-WP-Nonce': wpApiSettings.nonce }
});
const stats = await response.json();
console.log(stats.summary.pass_rate);
Example with Application Password
curl https://yoursite.com/wp-json/wkcft/v1/stats?range=7 \
-u "admin:xxxx xxxx xxxx xxxx xxxx xxxx"
GET /conditions — Read Conditions
Returns the current conditional rules config.
Request
GET /wp-json/wkcft/v1/conditions
Response
{
"skip_logged_in": "no",
"skip_known_customers": "no",
"require_after_failures": 0,
"ip_whitelist": [],
"ip_blacklist": [],
"country_whitelist": [],
"country_blacklist": []
}
Field reference:
| Field | Type | Notes |
|---|---|---|
skip_logged_in | "yes" | "no" | Bypass Turnstile for any logged-in user. |
skip_known_customers | "yes" | "no" | Bypass for customers with at least one completed order. |
require_after_failures | int (0–100) | Only require CAPTCHA after N failed login attempts per IP. 0 = always require. |
ip_whitelist | string[] | IPv4/IPv6 addresses or CIDR ranges to bypass. |
ip_blacklist | string[] | IPs/CIDR to always require. |
country_whitelist | string[] | Two-letter ISO country codes to bypass. |
country_blacklist | string[] | Two-letter ISO codes to always require. |
POST /conditions — Write Conditions
Update the conditional rules config.
Request
POST /wp-json/wkcft/v1/conditions
Content-Type: application/json
{
"config": {
"skip_logged_in": "yes",
"require_after_failures": 3,
"ip_whitelist": ["10.0.0.0/8", "203.0.113.42"]
}
}
Fields missing from the config object are dropped to their sanitized defaults — send the full object if you want a partial merge to be persisted as-is.
Response
Returns the sanitized, persisted config directly (not wrapped):
{
"skip_logged_in": "yes",
"skip_known_customers": "no",
"require_after_failures": 3,
"ip_whitelist": ["10.0.0.0/8", "203.0.113.42"],
"ip_blacklist": [],
"country_whitelist": [],
"country_blacklist": []
}
Error when config is not a JSON object:
{
"code": "rest_invalid_param",
"message": "config must be a JSON object.",
"data": { "status": 400 }
}
GET /design-studio — Read Design Config
Returns the current Design Studio array.
Request
GET /wp-json/wkcft/v1/design-studio
Response
{
"theme": "",
"size": "",
"language": "",
"alignment": "",
"margin_top": 0,
"margin_bot": 0,
"padding": 0,
"bg_color": "",
"border_color": "",
"border_w": 0,
"border_rad": 0,
"shadow": "none",
"label_text": "",
"label_color": "#1d2327",
"helper_text": "",
"helper_color": "#646970"
}
Field reference:
| Field | Type | Allowed values |
|---|---|---|
theme | enum | "" (default) · light · dark · auto |
size | enum | "" · normal · compact · flexible |
language | string | empty or ISO code matching /^[a-zA-Z-]{2,8}$/ |
alignment | enum | "" · left · center · right |
margin_top, margin_bot | int | 0–100 (px) |
padding | int | 0–40 (px) |
bg_color, border_color | hex string | empty = transparent |
border_w | int | 0–8 (px) |
border_rad | int | 0–32 (px) |
shadow | enum | none · subtle · medium · strong |
label_text | string | ≤ 120 chars |
helper_text | string | ≤ 200 chars |
label_color, helper_color | hex string | CSS colors |
POST /design-studio — Write Design Config
Update Design Studio settings.
Request
POST /wp-json/wkcft/v1/design-studio
Content-Type: application/json
{
"config": {
"theme": "dark",
"shadow": "medium"
}
}
Fields missing from the config object reset to their defaults — send the full object to guarantee untouched keys stay.
Response
Returns the sanitized, persisted config directly (not wrapped):
{
"theme": "dark",
"size": "",
"language": "",
"alignment": "",
"margin_top": 0,
"margin_bot": 0,
"padding": 0,
"bg_color": "",
"border_color": "",
"border_w": 0,
"border_rad": 0,
"shadow": "medium",
"label_text": "",
"label_color": "#1d2327",
"helper_text": "",
"helper_color": "#646970"
}
Common Patterns
Programmatic Multi-Site Rollout
Push the same Design Studio config to a fleet of stores via a shell script:
for SITE in store1.com store2.com store3.com; do
curl -X POST "https://$SITE/wp-json/wkcft/v1/design-studio" \
-u "admin:xxx" \
-H "Content-Type: application/json" \
-d '{"config":{"theme":"dark","label_text":"Secure"}}'
done
SPA Form Example (React)
import { Turnstile } from '@marsidev/react-turnstile';
function CheckoutForm() {
const [token, setToken] = useState('');
async function handleSubmit(e) {
e.preventDefault();
const verify = await fetch('/wp-json/wkcft/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const result = await verify.json();
if (!result.success) {
alert('CAPTCHA failed');
return;
}
// Proceed with your order submission
await placeOrder(formData);
}
return (
<form onSubmit={handleSubmit}>
...
<Turnstile siteKey="YOUR_SITE_KEY" onSuccess={setToken} />
<button type="submit">Place Order</button>
</form>
);
}
Custom REST Verification Hook
Register your own endpoint that internally calls the validator:
add_action('rest_api_init', function() {
register_rest_route('my-api/v1', '/submit-form', [
'methods' => 'POST',
'callback' => function($request) {
$token = $request->get_param('cf-turnstile-response');
$result = WKCFT_Validator::wkcft_instance()->wkcft_check($token, 'my-custom-form');
if (!$result['success']) {
return new WP_Error('captcha_failed', 'CAPTCHA failed', ['status' => 400]);
}
// Process your form
return ['success' => true];
},
'permission_callback' => '__return_true'
]);
});
Error Codes
Common REST errors:
| Code | HTTP | Meaning |
|---|---|---|
rest_forbidden | 401 | Not authenticated |
rest_cannot_view | 403 | Authenticated but lacks capability |
wkcft_missing_site_key | 400 | No site key on the request and none stored in settings |
wkcft_missing_secret_key | 400 | No secret key on the request and none stored in settings |
rest_invalid_param | 400 | config not a JSON object, or range not in [7, 30, 90] |
wkcft_rate_limited | 429 | /verify hit the 10-per-60s per-IP rolling limit |
wkcft_class_unavailable | 503 | Required module (Logger / Conditions / Design Studio) not loaded on the host |
Extensibility
Add your own routes under the same namespace:
add_action('rest_api_init', function() {
register_rest_route('wkcft/v1', '/my-extension', [
'methods' => 'GET',
'callback' => 'my_extension_callback',
'permission_callback' => function() {
return current_user_can('manage_woocommerce');
}
]);
});
Troubleshooting
| Problem | Fix |
|---|---|
404 on /verify | Permalinks may be flat. Go to Settings → Permalinks → Save |
401 on /stats | Send X-WP-Nonce header (from wp_rest nonce) or use Application Password |
| 403 after auth | Your account lacks manage_woocommerce capability |
/verify returns success: false with no error-codes | Secret Key is missing — set on API Settings tab |
Rate-limited on /verify | Increase the limit via filter wkcft_rate_limit_threshold (if your use case is legitimate) |
Related Pages
- Filters & Hooks — Customize validation and limits
- Shortcode — Alternative to REST for custom forms
- Conditional Rules — Backing data for
/conditions - Design Studio — Backing data for
/design-studio
