Framework-agnostic PHP client for Preverus backend enforcement.
Use this package when your application already loads the hosted Preverus browser script and your backend needs to make trusted server-side calls with a private server key.
composer require preverus/preverus-phpRequires PHP 8.1+ and ext-json.
For server-rendered or non-JS-build sites, load the hosted script in your HTML:
<script
src="https://cdn.preverus.com/v1/preverus.js"
data-preverus-key="pk_live_xxx"
data-preverus-auto="true"
data-preverus-track-forms="true"
></script>
<form method="POST" action="/register" data-preverus-action="signup">
<input name="email" type="email">
<button type="submit">Create account</button>
</form>Before submit, the script attaches:
preverus_fingerprint
preverus_visitor_id
preverus_risk_session_token
preverus_browser_session_event_id
Your backend sends those fields to Preverus with your private server key before approving sensitive actions.
Never put your server key in browser code.
use Preverus\Client;
$client = new Client($_ENV['PREVERUS_SERVER_KEY']);
$decision = $client->decisions()->evaluate([
'event_type' => 'signup',
'user_id' => 'acct_42',
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'risk_session_token' => $_POST['preverus_risk_session_token'] ?? null,
'fingerprint' => $_POST['preverus_fingerprint'] ?? null,
'metadata' => [
'email' => $_POST['email'] ?? null,
'browser_session_event_id' => $_POST['preverus_browser_session_event_id'] ?? null,
],
], visitorId: $_POST['preverus_visitor_id'] ?? null);
if ($decision->isBlock()) {
http_response_code(403);
exit('Unable to create account.');
}
if ($decision->isReview()) {
// Trigger step-up auth, hold the action, or route to manual review.
}
// Continue signup, login, checkout, withdrawal, or payout.Prefer risk_session_token when available. It references the stored browser session collected by the hosted script. The token lets Preverus use the full browser/device context without your backend handling raw browser feature vectors.
$client = new Client($_ENV['PREVERUS_SERVER_KEY'], [
'endpoint' => 'https://api.preverus.com',
'timeout_ms' => 1500,
'connect_timeout_ms' => 500,
'retries' => 2,
'retry_delay_ms' => 150,
'max_retry_delay_ms' => 1000,
]);Default retry behavior is intentionally conservative for web requests. The client retries transient network failures and these statuses:
408, 409, 425, 429, 500, 502, 503, 504
The client does not retry validation or authentication errors such as 400, 401, 403, or 422.
For POST calls, pass an idempotency key when the operation may be retried:
$decision = $client->decisions()->evaluate(
input: [...],
visitorId: $visitorId,
idempotencyKey: 'signup:acct_42:' . bin2hex(random_bytes(16)),
);$decision = $client->decisions()->evaluate([
'event_type' => 'withdraw',
'user_id' => 'acct_42',
'ip' => '203.0.113.10',
'risk_session_token' => 'signed_token_from_browser',
'metadata' => [
'payment_address' => '0xabc123',
],
], visitorId: 'v_abc123');
$decision->recommendedAction(); // allow, review, block, or deny
$decision->isAllow();
$decision->isReview();
$decision->isBlock();
$decision->toArray();Recommended production behavior:
allow -> proceed
review -> step-up auth, hold, or manual review
block -> deny or hard challenge
Use events for fraud-relevant activity that does not need a synchronous decision.
$event = $client->events()->create([
'event_type' => 'login',
'user_id' => 'acct_42',
'ip' => '203.0.113.10',
'fingerprint' => $fingerprint,
'metadata' => [
'email' => 'person@example.com',
],
], visitorId: 'v_abc123');$visitor = $client->visitors()->lookup(visitorId: 'v_abc123');
$visitor = $client->visitors()->lookup(fingerprint: 'fp_hash');$result = $client->metadata()->lookup('email', 'person@example.com');
$graph = $client->metadata()->graph('v_abc123');Use metadata lookup for values such as email, phone, username, and payment address when you want to understand reuse across users or visitors.
$valid = $client->webhooks()->verify(
rawBody: file_get_contents('php://input'),
timestamp: $_SERVER['HTTP_X_FRAUD_WEBHOOK_TIMESTAMP'] ?? '',
signatureHeader: $_SERVER['HTTP_X_FRAUD_WEBHOOK_SIGNATURE'] ?? '',
secret: $_ENV['PREVERUS_WEBHOOK_SECRET'],
);
if (!$valid) {
http_response_code(400);
exit('Invalid signature');
}Reject webhook requests when:
- The signature is missing or invalid.
- The timestamp is older than 5 minutes.
- The webhook ID has already been processed.
Webhook delivery is at-least-once, so store X-Fraud-Webhook-Id or payload id as an idempotency key.
You can also verify, parse, and dispatch events in one flow:
$event = $client->webhooks()->constructEvent(
rawBody: file_get_contents('php://input'),
headers: [
'X-Fraud-Webhook-Timestamp' => $_SERVER['HTTP_X_FRAUD_WEBHOOK_TIMESTAMP'] ?? '',
'X-Fraud-Webhook-Signature' => $_SERVER['HTTP_X_FRAUD_WEBHOOK_SIGNATURE'] ?? '',
],
secret: $_ENV['PREVERUS_WEBHOOK_SECRET'],
);
if (alreadyProcessed($event->id())) {
http_response_code(204);
exit;
}
$client->webhooks()->dispatch($event, [
'decision.high_risk' => function ($event) {
// Open a case, notify ops, or hold the account action.
},
'*' => function ($event) {
// Optional catch-all handler.
},
]);The core client throws exceptions for API and network failures:
use Preverus\Exceptions\ApiException;
use Preverus\Exceptions\NetworkException;
try {
$decision = $client->decisions()->evaluate([...]);
} catch (ApiException $error) {
// Authentication, validation, rate limit, or server response.
$status = $error->statusCode();
$code = $error->errorCode();
} catch (NetworkException $error) {
// Timeout, DNS, TLS, or connection failure.
}If you want built-in fail-open/fail-review/fail-closed fallback decisions, use preverus/preverus-laravel or implement the same policy in your framework.
- Use a browser key only in HTML or frontend code.
- Use a server key only on your backend.
- Prefer
risk_session_tokenfor decisions when present. - Include
X-Visitor-IDwhen available. - Send your real customer account ID as
user_id. - Include request IP and useful metadata such as email, phone, username, and payment address.
- Use idempotency keys for retried POST requests.
- Treat
reviewas step-up/manual review, not as an automatic allow.