Developer documentation
Two steps: the widget in your form, then a server-side check of the token before you process anything. The API answers like reCAPTCHA and Turnstile, plus an action and reasons.
Quick start
1. Add the widget to your form. Replace the site key with your own from the dashboard.
<script src="https://formconfirmed.com/v1/widget.js" async defer></script>
<form method="post" action="/contact">
...
<div class="fcp-widget" data-sitekey="fcp_pub_..." data-purpose="contact"></div>
<button type="submit">Send</button>
</form>
2. Before sending or storing the submission, send the token to /v1/verify from your server. The secret key stays on the server.
<?php
$ch = curl_init('https://formconfirmed.com/v1/verify');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 3,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => getenv('FCP_SECRET'),
'response' => $_POST['fcp-response'] ?? '',
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
'content' => $_POST['message'] ?? '',
'expected_purpose' => 'contact',
]),
]);
$res = json_decode((string)curl_exec($ch), true);
if (!is_array($res) || !isset($res['success'], $res['action'])) {
$decision = 'hold'; // outage: never a pass
} elseif (!$res['success']) {
$decision = 'reject';
} else {
$decision = $res['action'] === 'flag' ? 'hold' : 'accept';
}
POST /v1/verify
https://formconfirmed.com/v1/verify
Send the fields as form data (application/x-www-form-urlencoded) or as JSON. Use a short timeout (3 seconds).
| Field | Meaning | |
|---|---|---|
secret | required | Your secret key (fcp_sec_…). Never put it in the page. |
response | required | The token from the form field fcp-response. Send it even if it is empty – do not reject a missing token yourself. |
remoteip | optional | IP address of the visitor. Sharpens rate and network signals. |
content | optional | Message text. Spam pre-filter in every plan, AI content check depending on plan and settings. Checked in memory, not stored. |
email | optional | E-mail address from the form: disposable providers and domains without mail are recognised. |
hp | optional | Value of the hidden field fcp-website (honeypot). |
expected_purpose | optional | The purpose the widget was signed for, e.g. contact, login, register. A token for another purpose is rejected. |
operation_id | optional | Stable ID of the business action (16–128 characters) – repeated requests do not count twice. Only together with expected_purpose. |
recipient | optional | Recipient of an action mail (e.g. password reset) for the per-recipient limits. |
form_mode | optional | interactive (default) or deferred for forms that are sent later. |
Response
{ "success": true, "score": 0.92, "action": "allow", "reasons": [],
"error-codes": [], "challenge_ts": "2026-11-06T09:41:12Z", "hostname": "example.com" }
success | false if the action is block or the request is invalid. |
score | 0.0 (bot) to 1.0 (human). |
action | allow, flag or block – the recommended handling. |
reasons | Signals that led to the result, e.g. too_fast, honeypot_filled. |
error-codes | Technical errors, see below. |
challenge_ts | Time the task was solved (UTC). |
hostname | Host on which the widget ran. |
content | Only with content check: category and confidence. |
Handling the action
- allow: process the submission normally.
- flag: do not process it silently – deliver it marked (spam folder, moderation).
- block: reject it and show a neutral error message.
When the service cannot be reached
A timeout, network error, 5xx or invalid answer is never a pass. Hold the message marked for moderation or show an error asking to try again. Accept it normally only after an independent check of your own, for example an existing account check. In the dashboard you can set the outage behaviour per site.
Error codes
missing-input-secret | No secret key sent. |
invalid-input-secret | Unknown or revoked secret key. |
missing-input-response | No token sent. |
invalid-input-response | Token invalid, expired or already used. |
purpose-mismatch | The token was issued for another purpose. |
purpose-not-allowed | This purpose is not allowed for the site (dashboard setting). |
site-paused | The site is paused in the dashboard. |
service-disabled | The service is switched off temporarily. |
bad-request | Fields missing, too long or in the wrong format. |
service-unavailable | The widget could not reach the service – treat it like an outage. |
Forms sent with JavaScript
For forms sent with fetch or XMLHttpRequest, get the token first and send it in the field fcp-response:
form.addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(form);
data.set('fcp-response', await window.__fcpWidget.token(form));
await fetch(form.action, { method: 'POST', body: data });
});
WordPress
The formexposed plugin (version 1.8 or later) adds the widget and the server check to Contact Form 7, WPForms and comments automatically. Enter the site key and the secret key under Plugins → formexposed → Form protection.
Data protection
No cookies, nothing stored on the device, no fingerprint. Processed: the solved task, the filling time, whether the hidden field was filled and – if you send it – the message text, which is only checked in memory. IP addresses only shortened and as a daily changing hash. A text for your privacy policy is in the dashboard.