Server-side verification reference
The browser widget is responsible only for collecting a human-verification response. Your application must send that response to VettiGuard from its backend and must not perform the protected operation until the verification response has been validated.
Verification endpoint
POST https://vettiguard.example.com/api/v1/siteverify
For a local XAMPP installation:
POST http://localhost/vettiguard/api/v1/siteverify
The endpoint accepts either:
application/x-www-form-urlencoded; orapplication/json.
The private secret must be stored only on the application server. Never place it in HTML, JavaScript, mobile application bundles, public repositories, browser requests, or client-side environment files.
Required request values
| Field | Required | Description |
|---|
secret | Yes | Private secret belonging to the protected site. New keys normally begin with vg_secret_. |
response | Yes | Short-lived token generated by the widget in the vettiguard-response form field. |
remoteip | Recommended | User IP address observed by your backend. When supplied, it must match the IP bound to the token. |
action | Recommended | Exact workflow name supplied to the widget, such as account-registration or checkout. |
The action should be a stable, narrowly scoped identifier. Do not reuse one generic action for unrelated workflows.
This is the recommended command-line format because it avoids manually constructing JSON and safely URL-encodes each value.
Linux and macOS
export VETTIGUARD_VERIFY_URL="https://vettiguard.example.com/api/v1/siteverify"
export VETTIGUARD_SECRET_KEY="vg_secret_replace_me"
export VETTIGUARD_RESPONSE_TOKEN="replace_with_widget_token"
export VISITOR_IP="203.0.113.10"
export EXPECTED_ACTION="account-registration"
curl --silent --show-error --fail-with-body \
--request POST "$VETTIGUARD_VERIFY_URL" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "secret=$VETTIGUARD_SECRET_KEY" \
--data-urlencode "response=$VETTIGUARD_RESPONSE_TOKEN" \
--data-urlencode "remoteip=$VISITOR_IP" \
--data-urlencode "action=$EXPECTED_ACTION"
Windows Command Prompt
set VETTIGUARD_VERIFY_URL=http://localhost/vettiguard/api/v1/siteverify
set VETTIGUARD_SECRET_KEY=vg_secret_replace_me
set VETTIGUARD_RESPONSE_TOKEN=replace_with_widget_token
set VISITOR_IP=127.0.0.1
set EXPECTED_ACTION=account-registration
curl.exe --silent --show-error --fail-with-body ^
--request POST "%VETTIGUARD_VERIFY_URL%" ^
--header "Content-Type: application/x-www-form-urlencoded" ^
--data-urlencode "secret=%VETTIGUARD_SECRET_KEY%" ^
--data-urlencode "response=%VETTIGUARD_RESPONSE_TOKEN%" ^
--data-urlencode "remoteip=%VISITOR_IP%" ^
--data-urlencode "action=%EXPECTED_ACTION%"
Windows PowerShell
Use curl.exe, not the PowerShell alias for Invoke-WebRequest:
$verifyUrl = 'http://localhost/vettiguard/api/v1/siteverify'
$secret = 'vg_secret_replace_me'
$responseToken = 'replace_with_widget_token'
$visitorIp = '127.0.0.1'
$expectedAction = 'account-registration'
curl.exe --silent --show-error --fail-with-body `
--request POST $verifyUrl `
--header 'Content-Type: application/x-www-form-urlencoded' `
--data-urlencode "secret=$secret" `
--data-urlencode "response=$responseToken" `
--data-urlencode "remoteip=$visitorIp" `
--data-urlencode "action=$expectedAction"
cURL: JSON request
curl --silent --show-error --fail-with-body \
--request POST "https://vettiguard.example.com/api/v1/siteverify" \
--header "Content-Type: application/json" \
--data '{
"secret": "vg_secret_replace_me",
"response": "replace_with_widget_token",
"remoteip": "203.0.113.10",
"action": "account-registration"
}'
When values come from variables, use a JSON encoder rather than string concatenation to prevent malformed JSON or shell-injection problems.
Successful verification response
HTTP status: 200 OK
{
"success": true,
"challenge_ts": "2026-07-30 21:42:18",
"hostname": "example.com",
"score": 0.82,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": "visual",
"challenge_variant": "identify",
"action": "account-registration",
"error-codes": []
}
A successful response should be accepted only after all application-level checks pass.
Required validation sequence
- Confirm the HTTP request completed within your timeout.
- Confirm the response body is valid JSON.
- Require
success to be exactly true. - Require
error-codes to be empty. - Require
action to equal the action expected for the current handler. - Require
hostname to equal an explicitly allowed hostname. - Inspect
score_enforced. When it is false (score-based mode), apply your own action-specific score threshold. When it is true, you may still enforce a stricter application threshold. - Optionally inspect
challenge_ts for logging and additional freshness checks. - Execute the protected business operation only after all checks pass.
Interactive and policy-managed modes enforce the protected site's configured score_threshold before success can be true. Score-based mode intentionally returns score_enforced: false; it reports the score without making the business decision. Your backend must apply an action-specific threshold in that mode.
Meaning of response properties
| Property | Type | Meaning |
|---|
success | Boolean | Final VettiGuard verification decision. |
challenge_ts | String or null | UTC/database timestamp when the response token was issued. |
hostname | String or null | Hostname bound to the original browser challenge. |
score | Number | Risk confidence from 0.000 to 1.000; higher is more trustworthy. |
score_threshold | Number | Configured score threshold. In score-based mode this is a recommendation rather than an automatic rejection threshold. |
score_enforced | Boolean | true when VettiGuard applied the site threshold; false when the customer backend must decide. |
challenge_type | String or null | Mode that produced the response token, such as checkbox, visual, or score_based. |
action | String or null | Action bound to the token by the browser challenge. |
error-codes | Array | Token-validation and policy failures. |
Score-based verification response
Score-based mode never opens an interactive challenge and does not automatically reject a valid token for a low score. A successful token verification may therefore look like this:
{
"success": true,
"challenge_ts": "2026-07-31 18:20:00",
"hostname": "example.com",
"score": 0.34,
"score_threshold": 0.5,
"score_enforced": false,
"challenge_type": "score_based",
"action": "account-registration",
"error-codes": []
}
This response means the token, action, hostname, and expiry checks passed. It does not mean the application should automatically accept the business action. Apply your own score policy before continuing.
Important HTTP-status behaviour
VettiGuard separates malformed/configuration requests from normal token-validation failures.
| HTTP status | Meaning | Example |
|---|
200 | The verification request was processed. Inspect success and error-codes. | Valid token, expired token, duplicate token, low score, action mismatch. |
400 | Required request data or secret credentials are missing/invalid. | missing-input-secret, invalid-input-secret, missing-input-response. |
429 | Server-side verification rate limit exceeded. | rate-limit-exceeded. |
503 | VettiGuard dependency or configuration failure. Fail closed and alert operators. | database-unavailable, database-schema-outdated, rate-limiter-unavailable. |
Do not treat every HTTP 200 response as successful. A rejected or consumed token is deliberately returned as HTTP 200 with success: false.
Token-validation failure responses
HTTP status: 200 OK
{
"success": false,
"challenge_ts": null,
"hostname": null,
"score": 0,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": null,
"action": null,
"error-codes": [
"invalid-input-response"
]
}
The user must complete a new challenge. Do not retry the same token.
Expired or already-consumed token
HTTP status: 200 OK
{
"success": false,
"challenge_ts": "2026-07-30 21:42:18",
"hostname": "example.com",
"score": 0.82,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": "visual",
"challenge_variant": "identify",
"action": "account-registration",
"error-codes": [
"timeout-or-duplicate"
]
}
This response covers both token expiry and replay. Reset the widget and obtain a fresh token.
Action mismatch
HTTP status: 200 OK
{
"success": false,
"challenge_ts": "2026-07-30 21:42:18",
"hostname": "example.com",
"score": 0.82,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": "visual",
"challenge_variant": "identify",
"action": "contact-form",
"error-codes": [
"action-mismatch"
]
}
The returned action is the action originally bound to the token. The token is consumed by this verification attempt.
Remote-IP mismatch
HTTP status: 200 OK
{
"success": false,
"challenge_ts": "2026-07-30 21:42:18",
"hostname": "example.com",
"score": 0.82,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": "visual",
"challenge_variant": "identify",
"action": "account-registration",
"error-codes": [
"remoteip-mismatch"
]
}
This occurs only when remoteip is supplied and does not match the IP bound to the token. Ensure your application and VettiGuard use the same trusted proxy/client-IP policy.
Low score (score-enforced modes)
HTTP status: 200 OK
{
"success": false,
"challenge_ts": "2026-07-30 21:42:18",
"hostname": "example.com",
"score": 0.34,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": "visual",
"challenge_variant": "identify",
"action": "account-registration",
"error-codes": [
"low-score"
]
}
The response token is consumed. Ask the user to complete a fresh challenge or apply a suitable step-up verification flow. Score-based mode does not return low-score; the customer backend evaluates its reported score instead.
Multiple policy failures
More than one error can be returned:
{
"success": false,
"challenge_ts": "2026-07-30 21:42:18",
"hostname": "example.com",
"score": 0.31,
"score_threshold": 0.5,
"score_enforced": true,
"challenge_type": "visual",
"challenge_variant": "identify",
"action": "contact-form",
"error-codes": [
"action-mismatch",
"remoteip-mismatch",
"low-score"
]
}
Treat the response as a single failed verification. Do not disclose detailed risk information to the visitor.
Request and credential error responses
Missing secret
HTTP status: 400 Bad Request
{
"success": false,
"error": "missing-input-secret"
}
Invalid secret
HTTP status: 400 Bad Request
{
"success": false,
"error": "invalid-input-secret"
}
Missing response token
HTTP status: 400 Bad Request
{
"success": false,
"error": "missing-input-response"
}
Verification rate limit exceeded
HTTP status: 429 Too Many Requests
{
"success": false,
"error": "rate-limit-exceeded"
}
Apply controlled backoff. Do not repeatedly submit the same one-time token.
Temporary service/configuration failure
HTTP status: 503 Service Unavailable
{
"success": false,
"error": "database-unavailable"
}
Other possible operational errors include:
app-key-misconfigured
database-schema-outdated
rate-limiter-unavailable
challenge-creation-failed
Fail closed, record the provider error internally, and show the visitor a neutral message such as “Verification is temporarily unavailable.”
Error-code reference
| Error code | Category | Recommended handling |
|---|
missing-input-secret | Request | Fix server integration; do not retry from the browser. |
invalid-input-secret | Credential | Confirm the secret belongs to the same site key and environment. Rotate if exposed. |
missing-input-response | Request | Require completion of the widget before form submission. |
invalid-input-response | Token | Reset the widget and request a new response. |
timeout-or-duplicate | Token | Token expired or was consumed; request a new response. |
action-mismatch | Policy | Reject, investigate integration mismatch, and request a new response. |
remoteip-mismatch | Policy | Reject and review trusted-proxy/client-IP configuration. |
low-score | Risk | Reject or require a fresh/stronger challenge. |
rate-limit-exceeded | Capacity | Back off and inspect unexpected verification volume. |
app-key-misconfigured | Operations | Correct APP_KEY; fail closed. |
database-unavailable | Operations | Restore database connectivity; fail closed. |
database-schema-outdated | Operations | Run the required migration; fail closed. |
rate-limiter-unavailable | Operations | Restore the rate-limit store/database; fail closed. |
Raw PHP cURL implementation
The bundled SDK is recommended, but the following example shows the complete verification contract without the SDK:
<?php
declare(strict_types=1);
$verifyUrl = getenv('VETTIGUARD_VERIFY_URL') ?: 'https://vettiguard.example.com/api/v1/siteverify';
$secret = getenv('VETTIGUARD_SECRET_KEY') ?: '';
$token = trim((string) ($_POST['vettiguard-response'] ?? ''));
$expectedAction = 'account-registration';
$allowedHostnames = ['example.com', 'www.example.com'];
$minimumScore = 0.50;
if ($secret === '' || $token === '') {
http_response_code(422);
exit('Please complete the verification challenge.');
}
$payload = http_build_query([
'secret' => $secret,
'response' => $token,
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
'action' => $expectedAction,
]);
$curl = curl_init($verifyUrl);
if ($curl === false) {
http_response_code(503);
exit('Verification is temporarily unavailable.');
}
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
]);
$rawBody = curl_exec($curl);
$curlError = curl_error($curl);
$httpStatus = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if (!is_string($rawBody) || $rawBody === '' || $curlError !== '') {
error_log('VettiGuard transport failure: ' . $curlError);
http_response_code(503);
exit('Verification is temporarily unavailable.');
}
$result = json_decode($rawBody, true);
if (!is_array($result)) {
error_log('VettiGuard returned invalid JSON. HTTP ' . $httpStatus);
http_response_code(503);
exit('Verification is temporarily unavailable.');
}
if ($httpStatus !== 200) {
$providerError = (string) ($result['error'] ?? 'verification-request-failed');
error_log('VettiGuard request failed: ' . $providerError . ' (HTTP ' . $httpStatus . ')');
http_response_code($httpStatus === 429 ? 429 : 503);
exit('Verification is temporarily unavailable.');
}
$errors = is_array($result['error-codes'] ?? null) ? $result['error-codes'] : [];
$hostname = strtolower((string) ($result['hostname'] ?? ''));
$action = (string) ($result['action'] ?? '');
$score = (float) ($result['score'] ?? 0.0);
$valid = ($result['success'] ?? false) === true
&& $errors === []
&& hash_equals($expectedAction, $action)
&& in_array($hostname, $allowedHostnames, true)
&& $score >= $minimumScore;
if (!$valid) {
error_log('VettiGuard verification rejected: ' . json_encode([
'errors' => $errors,
'action' => $action,
'hostname' => $hostname,
'score' => $score,
], JSON_UNESCAPED_SLASHES));
http_response_code(422);
exit('Verification failed. Please try again.');
}
// The token is valid. Perform the protected business operation exactly once.
cURL response validation with jq
This example is useful for integration smoke tests. It fails unless the complete context is valid.
export EXPECTED_ACTION="account-registration"
export EXPECTED_HOSTNAME="example.com"
export MINIMUM_SCORE="0.50"
curl --silent --show-error --fail-with-body \
--request POST "$VETTIGUARD_VERIFY_URL" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "secret=$VETTIGUARD_SECRET_KEY" \
--data-urlencode "response=$VETTIGUARD_RESPONSE_TOKEN" \
--data-urlencode "remoteip=$VISITOR_IP" \
--data-urlencode "action=$EXPECTED_ACTION" \
| jq --exit-status '
.success == true
and (."error-codes" | length) == 0
and .action == env.EXPECTED_ACTION
and .hostname == env.EXPECTED_HOSTNAME
and .score >= (env.MINIMUM_SCORE | tonumber)
'
A zero exit code means all checks passed. A non-zero exit code means the protected operation must not proceed.
Token lifecycle and replay rules
- Response tokens are short-lived.
- Response tokens are single-use.
- A valid verification attempt consumes the token even when action, IP, or score policy fails.
- A token must not be verified early and then reused later for the protected operation.
- Validate ordinary form fields before calling
siteverify so a token is not wasted on an otherwise invalid submission. - Verify immediately before the protected write, account creation, payment initiation, message send, or privileged API call.
- If your business operation fails after verification, do not reuse the token. Ask the user to submit again with a fresh response.
Hostname validation
Use an explicit allowlist. Avoid loose suffix checks such as endsWith('example.com'), because they may accept unintended hostnames.
Recommended:
$allowedHostnames = ['example.com', 'www.example.com'];
if (!in_array(strtolower((string) $result['hostname']), $allowedHostnames, true)) {
// Reject.
}
For local testing, the expected hostname is usually localhost, 127.0.0.1, or the actual development hostname used by the browser.
Remote-IP validation behind a proxy
Supply remoteip only when your application can determine the real client IP safely.
- Keep
TRUST_PROXY=false when requests connect directly to the application server. - Enable trusted-proxy handling only behind a proxy that overwrites forwarded headers.
- Do not trust arbitrary
X-Forwarded-For values sent directly by visitors. - Ensure the protected website and VettiGuard observe the same effective client IP; otherwise
remoteip-mismatch will occur.
When reliable client-IP resolution is not available, omit remoteip rather than supplying an untrusted value. Action and hostname checks remain mandatory.
Recommended visitor messages
Do not expose internal provider details, numeric scores, secrets, thresholds, or policy failures to visitors. Scores belong in backend decisions and authenticated administration analytics, not in the visual completion response.
| Situation | Visitor-facing message |
|---|
| Missing/expired/duplicate token | “Please complete the verification again.” |
| Low score or policy mismatch | “Verification failed. Please try again.” |
| Rate limit | “Too many verification attempts. Please try again shortly.” |
| Service/configuration failure | “Verification is temporarily unavailable.” |
Log the detailed error code internally with a request/correlation identifier.
Production checklist
- Use HTTPS for both the protected application and VettiGuard.
- Keep the secret in server-side environment or secret-management storage.
- Use separate site credentials for production, staging, and local development.
- Set connect and total timeouts on the outbound request.
- Fail closed on timeout, invalid JSON, non-200 service responses, and validation failure.
- Validate
success, error-codes, action, hostname, and score. - Supply
remoteip only when resolved through a trusted network path. - Validate ordinary form data before consuming the token.
- Execute the protected action once and only after verification.
- Do not log the secret or complete response token.
- Monitor
low-score, timeout-or-duplicate, invalid-input-secret, rate-limit-exceeded, and HTTP 503 trends. - Rotate a secret immediately if it may have been disclosed.
docs/API.md — complete API surface and browser endpoints.docs/SAMPLE-USAGE.md — full contact-form integration.sdk/php/VettiGuardVerifier.php — reusable PHP verification SDK.examples/php-contact-form — copyable end-to-end example.