VettiGuard

Verify every response on your server.

The browser widget supplies a short-lived token. Your backend must exchange that token with VettiGuard, validate the returned context, and only then perform the protected action.

1 Receive token2 Verify privately3 Validate context
01

Send the secret and token from the backend.

Use --data-urlencode so every field is encoded safely. The private secret must never appear in browser code.

curl --silent --show-error --fail-with-body \
  --request POST "https://vettiguard.com/api/v1/siteverify" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "secret=vg_secret_replace_me" \
  --data-urlencode "response=RESPONSE_TOKEN" \
  --data-urlencode "remoteip=203.0.113.10" \
  --data-urlencode "action=account-registration"
02

JSON is supported as an alternative.

Use a real JSON encoder when values are dynamic. Avoid concatenating untrusted values into a JSON string.

curl --silent --show-error --fail-with-body \
  --request POST "https://vettiguard.com/api/v1/siteverify" \
  --header "Content-Type: application/json" \
  --data '{
    "secret": "vg_secret_replace_me",
    "response": "RESPONSE_TOKEN",
    "remoteip": "203.0.113.10",
    "action": "account-registration"
  }'
03

HTTP 200 is not the final decision.

Require explicit success, an empty error list, the expected action, an allowed hostname, and an action-appropriate score decision.

{
  "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": []
}

Fail closed unless the complete context matches.

The widget animation is not a trust decision. The backend response is the source of truth.

Success

Require success to be exactly true.

Error list

Require error-codes to be empty.

Action

Match the exact workflow expected by the current handler.

Hostname

Compare against an explicit hostname allowlist.

Score policy

When score_enforced is false, apply your own action-specific threshold.

Token lifecycle

Expired or consumed tokens require a fresh challenge.

Processed requests can still be rejected.

These responses use HTTP 200 because the verification endpoint processed the request. Inspect success and error-codes.

Expired or duplicate

{
  "success": false,
  "error-codes": ["timeout-or-duplicate"]
}

Reset the widget and request a new token.

Action mismatch

{
  "success": false,
  "action": "contact-form",
  "error-codes": ["action-mismatch"]
}

The token is consumed and cannot be retried.

Low score (enforced modes)

{
  "success": false,
  "score": 0.34,
  "score_threshold": 0.5,
  "error-codes": ["low-score"]
}

Reject or require a new, stronger challenge. Score-based mode returns the score without this error.

Remote-IP mismatch

{
  "success": false,
  "error-codes": ["remoteip-mismatch"]
}

Review the trusted-proxy and client-IP configuration.

Handle request and service errors separately.

StatusExample errorRequired handling
400missing-input-secret, invalid-input-secret, missing-input-responseFix the integration or obtain a new token. Do not run the protected action.
429rate-limit-exceededApply controlled backoff and inspect unexpected verification volume.
503database-unavailable, database-schema-outdated, rate-limiter-unavailableFail closed, alert operators, and show a neutral temporary-unavailability message.
04

Make the risk decision on your backend.

Score-based verification never displays a challenge and returns score_enforced: false. The token can be valid while its score is too low for your business action.

{
  "success": true,
  "hostname": "example.com",
  "score": 0.34,
  "score_threshold": 0.5,
  "score_enforced": false,
  "challenge_type": "score_based",
  "action": "account-registration",
  "error-codes": []
}
05

Set transport timeouts and validate every field.

The full package includes a production-oriented raw cURL example and a reusable PHP SDK.

$result = $verifier->verify(
    $_POST['vettiguard-response'] ?? '',
    $_SERVER['REMOTE_ADDR'] ?? null,
    'account-registration'
);

$allowedHosts = ['example.com', 'www.example.com'];

$valid = $result->isSuccess()
    && $result->errors() === []
    && $result->action() === 'account-registration'
    && in_array(strtolower((string) $result->hostname()), $allowedHosts, true)
    && $result->score() >= 0.50;

if (!$valid) {
    http_response_code(422);
    exit('Verification failed. Please try again.');
}

// Perform the protected action exactly once.

Windows commands, payloads, errors, proxies, and replay rules.

The full packaged verification document is rendered below, so every deployment detail is available directly in the browser.

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; or
  • application/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

FieldRequiredDescription
secretYesPrivate secret belonging to the protected site. New keys normally begin with vg_secret_.
responseYesShort-lived token generated by the widget in the vettiguard-response form field.
remoteipRecommendedUser IP address observed by your backend. When supplied, it must match the IP bound to the token.
actionRecommendedExact 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.

cURL: form-encoded request

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

  1. Confirm the HTTP request completed within your timeout.
  2. Confirm the response body is valid JSON.
  3. Require success to be exactly true.
  4. Require error-codes to be empty.
  5. Require action to equal the action expected for the current handler.
  6. Require hostname to equal an explicitly allowed hostname.
  7. 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.
  8. Optionally inspect challenge_ts for logging and additional freshness checks.
  9. 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

PropertyTypeMeaning
successBooleanFinal VettiGuard verification decision.
challenge_tsString or nullUTC/database timestamp when the response token was issued.
hostnameString or nullHostname bound to the original browser challenge.
scoreNumberRisk confidence from 0.000 to 1.000; higher is more trustworthy.
score_thresholdNumberConfigured score threshold. In score-based mode this is a recommendation rather than an automatic rejection threshold.
score_enforcedBooleantrue when VettiGuard applied the site threshold; false when the customer backend must decide.
challenge_typeString or nullMode that produced the response token, such as checkbox, visual, or score_based.
actionString or nullAction bound to the token by the browser challenge.
error-codesArrayToken-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 statusMeaningExample
200The verification request was processed. Inspect success and error-codes.Valid token, expired token, duplicate token, low score, action mismatch.
400Required request data or secret credentials are missing/invalid.missing-input-secret, invalid-input-secret, missing-input-response.
429Server-side verification rate limit exceeded.rate-limit-exceeded.
503VettiGuard 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

Unknown or malformed response token

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 codeCategoryRecommended handling
missing-input-secretRequestFix server integration; do not retry from the browser.
invalid-input-secretCredentialConfirm the secret belongs to the same site key and environment. Rotate if exposed.
missing-input-responseRequestRequire completion of the widget before form submission.
invalid-input-responseTokenReset the widget and request a new response.
timeout-or-duplicateTokenToken expired or was consumed; request a new response.
action-mismatchPolicyReject, investigate integration mismatch, and request a new response.
remoteip-mismatchPolicyReject and review trusted-proxy/client-IP configuration.
low-scoreRiskReject or require a fresh/stronger challenge.
rate-limit-exceededCapacityBack off and inspect unexpected verification volume.
app-key-misconfiguredOperationsCorrect APP_KEY; fail closed.
database-unavailableOperationsRestore database connectivity; fail closed.
database-schema-outdatedOperationsRun the required migration; fail closed.
rate-limiter-unavailableOperationsRestore 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.

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.

SituationVisitor-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.

Deployment guidance is available directly in VettiGuard.

Browse the API contract and PHP SDK online instead of trying to open protected package files from the browser.

Get site credentials