VettiGuard

Verify VettiGuard responses in PHP.

The dependency-free verifier supports cURL and PHP streams, returns a typed result object, and keeps the private site secret entirely on your server.

PHP 8.2+ · no dependencies
01

Copy the verifier into your application.

Place VettiGuardVerifier.php outside your public web root where possible, then require it from the form handler that performs server-side validation.

require __DIR__ . '/VettiGuardVerifier.php';

use VettiGuardSdk\VettiGuardVerifier;
02

Configure the private endpoint and secret.

The secret must come from server-side configuration. Never place it in HTML, browser JavaScript, logs, or client-visible error messages.

$verifier = new VettiGuardVerifier(
    'https://vettiguard.com/api/v1/siteverify',
    getenv('VETTIGUARD_SECRET_KEY') ?: '',
    12
);
03

Validate the token and response context.

Require success, the expected action, an allowed hostname, and the score policy that applies to the protected operation.

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

if (!$result->isSuccess()
    || $result->action() !== 'account-registration'
    || $result->hostname() !== 'example.com'
    || $result->score() < 0.50) {
    http_response_code(422);
    exit('Verification failed. Please try again.');
}

// Perform the protected action only here.

Read the complete verification result.

The result object exposes the fields required to make a server-side decision.

isSuccess()

Whether VettiGuard accepted the response token.

errors()

Validation or transport error codes returned by the service.

action()

The action bound to the original widget request.

hostname()

The hostname that completed the browser challenge.

score()

The server-only risk score from 0.0 to 1.0.

isScoreEnforced()

Whether VettiGuard already enforced the site threshold.

challengeType()

The verification mode that produced the token.

challengeVariant()

The visual variant, such as identify or drag-and-drop.

toArray()

The complete decoded verification payload.

Review or copy the bundled verifier.

This is the same source shipped in the VettiGuard application package.

<?php
declare(strict_types=1);

namespace VettiGuardSdk;

final class VerificationResult
{
    public function __construct(private readonly array $payload)
    {
    }

    public function isSuccess(): bool
    {
        return (bool) ($this->payload['success'] ?? false);
    }

    public function score(): float
    {
        return (float) ($this->payload['score'] ?? 0.0);
    }

    public function scoreThreshold(): float
    {
        return (float) ($this->payload['score_threshold'] ?? 0.0);
    }

    public function isScoreEnforced(): bool
    {
        return (bool) ($this->payload['score_enforced'] ?? true);
    }

    public function challengeType(): ?string
    {
        $type = $this->payload['challenge_type'] ?? null;
        return is_string($type) && $type !== '' ? $type : null;
    }

    public function challengeVariant(): ?string
    {
        $variant = $this->payload['challenge_variant'] ?? null;
        return is_string($variant) && $variant !== '' ? $variant : null;
    }

    public function challengeTimestamp(): ?string
    {
        $timestamp = $this->payload['challenge_ts'] ?? null;
        return is_string($timestamp) && $timestamp !== '' ? $timestamp : null;
    }

    public function action(): ?string
    {
        $action = $this->payload['action'] ?? null;
        return is_string($action) && $action !== '' ? $action : null;
    }

    public function hostname(): ?string
    {
        $hostname = $this->payload['hostname'] ?? null;
        return is_string($hostname) && $hostname !== '' ? $hostname : null;
    }

    public function errors(): array
    {
        $errors = $this->payload['error-codes'] ?? [];
        if (is_array($errors) && $errors !== []) {
            return array_values(array_map('strval', $errors));
        }
        $singleError = $this->payload['error'] ?? null;
        return is_string($singleError) && $singleError !== '' ? [$singleError] : [];
    }

    public function toArray(): array
    {
        return $this->payload;
    }
}

final class VettiGuardVerifier
{
    public function __construct(
        private readonly string $verifyUrl,
        private readonly string $secret,
        private readonly int $timeoutSeconds = 10
    ) {
        if (!filter_var($verifyUrl, FILTER_VALIDATE_URL)) {
            throw new \InvalidArgumentException('A valid verification URL is required.');
        }
        if ($secret === '') {
            throw new \InvalidArgumentException('The CAPTCHA secret is required.');
        }
    }

    public function verify(string $responseToken, ?string $remoteIp = null, ?string $action = null): VerificationResult
    {
        if (trim($responseToken) === '') {
            return new VerificationResult(['success' => false, 'error-codes' => ['missing-input-response']]);
        }

        $payload = [
            'secret' => $this->secret,
            'response' => trim($responseToken),
        ];
        if ($remoteIp !== null && trim($remoteIp) !== '') $payload['remoteip'] = trim($remoteIp);
        if ($action !== null && trim($action) !== '') $payload['action'] = trim($action);

        $body = http_build_query($payload);
        $raw = function_exists('curl_init') ? $this->requestWithCurl($body) : $this->requestWithStreams($body);
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return new VerificationResult(['success' => false, 'error-codes' => ['invalid-verification-response']]);
        }
        return new VerificationResult($decoded);
    }

    private function requestWithCurl(string $body): string
    {
        $handle = curl_init($this->verifyUrl);
        if ($handle === false) {
            return '{}';
        }
        curl_setopt_array($handle, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $body,
            CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => $this->timeoutSeconds,
            CURLOPT_TIMEOUT => $this->timeoutSeconds,
        ]);
        $response = curl_exec($handle);
        $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
        curl_close($handle);
        return is_string($response) && $status >= 200 && $status < 500 ? $response : '{}';
    }

    private function requestWithStreams(string $body): string
    {
        $context = stream_context_create(['http' => [
            'method' => 'POST',
            'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
            'content' => $body,
            'timeout' => $this->timeoutSeconds,
            'ignore_errors' => true,
        ]]);
        $response = @file_get_contents($this->verifyUrl, false, $context);
        return is_string($response) ? $response : '{}';
    }
}
Server verification guide

cURL, response validation, errors, replay rules, and proxy handling.

API reference

Challenge, solve, asset, fallback, and site verification endpoints.