isSuccess()Whether VettiGuard accepted the response token.
The dependency-free verifier supports cURL and PHP streams, returns a typed result object, and keeps the private site secret entirely on your server.
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;
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
);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.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.
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 : '{}';
}
}
This action may affect your integration.