Docs menu
Guides
API reference
PHP SDK
The official PHP package, inlineshield/inlineshield-php: screen sign-ups, redeem browser tokens, send feedback, poll held sign-ups and verify webhooks, installed with Composer.
Last updated
Install
inlineshield/inlineshield-php 1.0.0 needs PHP 7.4 or later with the curl and json extensions. It installs from our own Composer repository at https://inlineshield.io/composer, whose packages.json pins the zip by its sha1. Classes are PSR-4 under InlineShield\.
composer config repositories.inlineshield composer https://inlineshield.io/composer
composer require inlineshield/inlineshield-php:^1.0
# Without Composer: unzip https://inlineshield.io/downloads/sdk/inlineshield-php-1.0.0.zip
# and require 'inlineshield-php/autoload.php';Or add the repository to composer.json yourself, then run composer update:
{
"repositories": [
{ "type": "composer", "url": "https://inlineshield.io/composer" }
],
"require": {
"inlineshield/inlineshield-php": "^1.0"
}
}Configure
Create one client with your secret API key and reuse it. Use a test key (bs_test_…) and the magic test addresses while you build: test screenings are not counted and send no webhooks. Without a key argument, the client reads INLINESHIELD_API_KEY from the environment; with no key at all it throws an auth error (MISSING_API_KEY).
<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
// Server-side only: your secret key (bs_live_… or bs_test_…) from the console, Settings › Keys.
$shield = new Client(getenv('INLINESHIELD_API_KEY'), [
'timeoutMs' => 3000, // per request (the default)
'failOpen' => true, // the default: an outage gives an "unavailable" result with action "allow"
]);| Option | Default | What it does |
|---|---|---|
'baseUrl' | https://api.inlineshield.io | The API origin. INLINESHIELD_BASE_URL from the environment, if set. |
'timeoutMs' | 3000 | Deadline of each request, in milliseconds: connecting, sending and reading the whole answer. |
'failOpen' | true | Return an unavailable result (action "allow") instead of throwing when InlineShield is unavailable. |
'maxRetries' | 1 | Retries after a failed connection (nothing was sent). |
'trustedProxies' | [] | Proxy addresses allowed to set X-Forwarded-For, for the visitor IP that verify() fills in. |
The base URL must use https; plain http is accepted only for localhost, for local tests. The timeout covers connecting and the whole answer (curl's total timeout). The SDK sets CURLOPT_NOSIGNAL, so with a curl built with the synchronous resolver a slow DNS lookup is not cut off by the timeout; most packaged curls use the threaded resolver (curl_version()['features'] & CURL_VERSION_ASYNCHDNS).
Methods
Results are the API's JSON as associative arrays, with the API's field names (see the API reference), plus unavailable: false for a real answer, true for a fail-open result.
| Method | What it does |
|---|---|
verify(['email' => ..., 'ip', 'userAgent', 'username', 'clientDwellMs', 'honeypot']) | POST /api/v1/verify. In a web request, 'ip' and 'userAgent' default to the visitor's. |
siteverify($token, $email) | POST /api/v1/siteverify: redeem the browser script token. |
feedback($requestId | ['token' => ...], $outcome, ['category' => ..., 'note' => ...]) | POST /api/v1/feedback: report fraud or legit. |
getReview($reviewId) | GET /api/v1/reviews/{reviewId}: the status of a held sign-up. |
Client::verifyWebhook($signatureHeader, $rawBody, $secret, $toleranceSec = 300) | Check a webhook delivery and return its event. Also Webhook::verify. |
Screen a sign-up
With the browser script on your form, redeem its token with siteverify before you create the account. When there is no token (NO_TOKEN), screen the sign-up on the server with verify instead. Any other error (INVALID_TOKEN, EXPIRED_TOKEN, TOKEN_ALREADY_USED, EMAIL_MISMATCH) means the token cannot be trusted: reject the sign-up. Then switch on action.
unavailable first. When InlineShield could not answer, the result has unavailable true, action "allow" (fail-open) and success false, because no token was redeemed, with the reason in error (for example TIMEOUT). Code that rejects every success false would reject all sign-ups during an outage. The order is: unavailable, then success, then error (NO_TOKEN: screen with verify; anything else: reject).<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
$shield = new Client(getenv('INLINESHIELD_API_KEY'));
// In your sign-up handler: the browser script put the token in the form.
function screen_signup(Client $shield, string $email): array
{
$checked = $shield->siteverify($_POST['betshield_token'] ?? null, $email);
if ($checked['success'] || $checked['unavailable']) {
return $checked;
}
if ($checked['error'] === 'NO_TOKEN') {
// The script was blocked, timed out or is missing: screen on the server instead
// (the SDK sends the visitor's IP and User-Agent from $_SERVER).
return $shield->verify(['email' => $email]);
}
return ['action' => 'block', 'error' => $checked['error']]; // INVALID_TOKEN, EXPIRED_TOKEN, ...
}
$result = screen_signup($shield, (string)($_POST['email'] ?? ''));
switch ($result['action']) {
case 'allow': // create the account
break;
case 'hold': // create it pending and keep $result['reviewId'] (live holds)
break;
case 'block': // reject the sign-up
break;
}Server-only screening
Without the browser script, call verify with the player's email, IP and User-Agent from your incoming request. See Server-only integration.
<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
$shield = new Client(getenv('INLINESHIELD_API_KEY'));
$result = $shield->verify([
'email' => (string)($_POST['email'] ?? ''),
'username' => $_POST['username'] ?? null, // optional
// 'ip' and 'userAgent' default to the visitor's (REMOTE_ADDR and the User-Agent header);
// behind a proxy, give the Client 'trustedProxies' or pass 'ip' yourself.
]);
// $result['action']: 'allow', 'hold' or 'block'; $result['riskScore'] 0-100; $result['reasons']
if ($result['unavailable']) {
// InlineShield could not answer: action is 'allow' (fail-open); $result['error'] says why.
}Outcome feedback
Report what really happened (fraud or legit) so InlineShield learns from your outcomes. Feedback is not counted toward your quota. See Outcome feedback.
<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
$shield = new Client(getenv('INLINESHIELD_API_KEY'));
// By the requestId InlineShield returned (accepted for screenings up to 180 days old):
$sent = $shield->feedback($requestId, 'fraud', ['category' => 'bonus_abuse', 'note' => 'Chargeback on first deposit']);
// Or by the browser token, for about a day after the screening:
$shield->feedback(['token' => $token], 'legit', ['category' => 'good_customer']);
// $sent['changed'] is false when the same feedback was already recorded.Review status
A live hold carries a reviewId. The signup.approved and signup.rejected webhooks tell you the decision; this call is the fallback. Test holds have no reviewId. See Hold and review.
<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
$shield = new Client(getenv('INLINESHIELD_API_KEY'));
// The reviewId of a held sign-up. Webhooks tell you first; this is the fallback.
$review = $shield->getReview($reviewId);
if ($review['status'] === 'approved') {
// activate the account
} elseif ($review['status'] === 'rejected') {
// delete or disable it ($review['decisionReason']: 'operator' or 'expired')
}Verify webhooks
Verify each delivery with the raw request body, before you parse it. The SDK computes HMAC-SHA256 of <t>.<raw body> with your endpoint's whole secret (whsec_…), compares it with v1 in constant time, refuses a t more than 300 seconds before or after now (the last argument changes the tolerance), and returns the parsed event. Read the body with file_get_contents('php://input'). See Webhooks.
<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
use InlineShield\InlineShieldWebhookException;
$rawBody = file_get_contents('php://input'); // the raw body, before json_decode
try {
$event = Client::verifyWebhook(
$_SERVER['HTTP_X_INLINESHIELD_SIGNATURE'] ?? null,
$rawBody,
getenv('INLINESHIELD_WEBHOOK_SECRET')
);
} catch (InlineShieldWebhookException $e) {
http_response_code(400); // $e->getErrorCode(): MALFORMED_HEADER, SIGNATURE_MISMATCH, ...
exit;
}
if ($event['event'] === 'signup.approved') {
// activate the account that has reviewId $event['data']['reviewId']
} elseif ($event['event'] === 'signup.rejected') {
// delete or disable it ($event['data']['decisionReason'])
}
// Deliveries can repeat: make this idempotent (key on $event['id']).
http_response_code(200);| Error code | Meaning |
|---|---|
NO_SECRET | The secret is empty or missing: the SDK never verifies with an empty key. |
MALFORMED_HEADER | The header is not exactly t=<seconds>,v1=<64 lower-case hex characters>. |
TIMESTAMP_OUT_OF_RANGE | The delivery was signed more than the tolerance (300 seconds by default) before or after now. |
SIGNATURE_MISMATCH | The HMAC does not match: another secret, or a changed body. |
INVALID_PAYLOAD | The signature matches but the body is not a JSON object. |
Errors
Fail-open is the default. When InlineShield cannot be reached, does not answer in time, answers with a server error, a rate limit or a quota block, the methods do not throw: they return an "unavailable" result with unavailable true and the reason in error. For verify and siteverify that result carries action "allow", so a sign-up goes through during an outage; getReview answers status "pending". Set 'failOpen' to false to get exceptions instead. A refused key and a refused request are always thrown. Only a failed connection, where nothing was sent, is retried (once, by default); a timeout or an answer is never retried, so a screening is never recorded twice.
| unavailable result error | When |
|---|---|
TIMEOUT | No answer within the timeout. |
UNAVAILABLE | No connection, a server error (5xx), or an answer that is not JSON. |
RATE_LIMIT_EXCEEDED | Too many requests (429). |
TEST_LIMIT | A test key is over its daily test limit (429). |
QUOTA_EXCEEDED | The account is over its monthly quota (402, or QUOTA_EXCEEDED from siteverify). |
RATE_LIMIT_EXCEEDED, TEST_LIMIT) or an exhausted quota (QUOTA_EXCEEDED) gives action "allow", so every further sign-up is admitted unscreened until the limit resets; a flood that exceeds your limit gets through. To choose otherwise, check result.error on an unavailable result and hold those sign-ups for review or reject them, or set 'failOpen' to false and handle the rate-limit and quota errors yourself.| Error class | When |
|---|---|
InlineShieldAuthException | A missing, unknown, revoked or expired key (401), or a suspended account (403 TENANT_SUSPENDED). Always thrown. |
InlineShieldApiException | A refused request: 400 BAD_REQUEST or INVALID_IP, 404 NOT_FOUND, 422 SCREENING_TOO_OLD, or a redirect (UNEXPECTED_REDIRECT). Always thrown. |
InlineShieldUnavailableException | Code TIMEOUT (InlineShieldTimeoutException) or UNAVAILABLE (InlineShieldServerException for 5xx). Thrown only with 'failOpen' => false. |
InlineShieldRateLimitException | Code RATE_LIMIT_EXCEEDED or TEST_LIMIT, with getRetryAfter() in seconds. Thrown only with 'failOpen' => false. |
InlineShieldQuotaException | Code QUOTA_EXCEEDED. Thrown only with 'failOpen' => false. |
InlineShieldWebhookException | Thrown by verifyWebhook. |
Every class extends InlineShieldException, with getErrorCode() (the error code) and getStatus() (the HTTP status, or null when there was no answer). The names are those of the single-file SDK, so existing catch blocks keep working. Error messages never contain your API key or an email address, and the SDK logs nothing. Every API error code is on Errors and HTTP codes.
<?php
require __DIR__ . '/vendor/autoload.php';
use InlineShield\Client;
use InlineShield\InlineShieldApiException;
use InlineShield\InlineShieldAuthException;
use InlineShield\InlineShieldQuotaException;
use InlineShield\InlineShieldRateLimitException;
use InlineShield\InlineShieldUnavailableException;
$shield = new Client(getenv('INLINESHIELD_API_KEY'));
try {
$result = $shield->verify(['email' => (string)($_POST['email'] ?? '')]);
if ($result['unavailable']) {
// Fail-open: $result['error'] is TIMEOUT, UNAVAILABLE, RATE_LIMIT_EXCEEDED, TEST_LIMIT or QUOTA_EXCEEDED.
}
} catch (InlineShieldAuthException $e) {
// INVALID_API_KEY, EXPIRED_API_KEY, TENANT_SUSPENDED...: fix the configuration (thrown even with failOpen).
throw $e;
} catch (InlineShieldApiException $e) {
// The request was refused: $e->getErrorCode() (e.g. INVALID_IP) and $e->getStatus().
throw $e;
}
// Fail-closed instead: with 'failOpen' => false an outage throws.
$strict = new Client(getenv('INLINESHIELD_API_KEY'), ['failOpen' => false]);
try {
$strict->verify(['email' => (string)($_POST['email'] ?? '')]);
} catch (InlineShieldUnavailableException | InlineShieldRateLimitException | InlineShieldQuotaException $e) {
// InlineShield is unavailable: reject the sign-up, or ask the player to try again ($e->getRetryAfter() on a rate limit).
}Single-file SDK
The older files from the console's Setup page keep working: inlineshield-guard.php (served at https://api.inlineshield.io/api/v1/sdk/php/guard) and InlineShield.php (served at https://api.inlineshield.io/api/v1/sdk/php/client). To move to the package, remove the require of InlineShield.php (both define InlineShield\Client, so load only one). The package keeps the file's evaluate() method, its exception names and its 'endpoint' and 'failMode' options. It never writes to the error log, and it retries only a failed connection.
Without Composer, unzip inlineshield-php-1.0.0.zip (https://inlineshield.io/downloads/sdk/inlineshield-php-1.0.0.zip) and require its autoload.php.