Sign-up fraud screening REST API
The InlineShield REST API screens a sign-up before your server creates the account. Use a browser script plus one server call to /api/v1/siteverify, or a single server-only call to /api/v1/verify. Both return a verdict, a risk score, triggers, plain-language reasons and a recommended action you can act on in a few lines of code.
Last updated
Option 1: browser script and siteverify
- Add the script to your sign-up page, with the site key from the console. It checks the browser and adds a token to the form as the field
betshield_token. - When the form arrives, your server sends the token and the email to
POST /api/v1/siteverifywith your secret API key in theX-Api-Keyheader. - Create the account, hold it, or refuse it based on the answer.
<script src="https://api.inlineshield.io/inlineshield.js" data-sitekey="YOUR_SITE_KEY" async></script>Server code, ready to paste
These are the same snippets the console's Setup page gives you, with your keys filled in there. Each one calls /api/v1/siteverify, falls back to /api/v1/verify when the browser token is missing, and accepts the sign-up if InlineShield can't be reached unless you switch it to reject.
# $TOKEN is the betshield_token form field, $EMAIL the submitted email
curl -X POST https://api.inlineshield.io/api/v1/siteverify \
-H "X-Api-Key: $INLINESHIELD_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"token\": \"$TOKEN\", \"email\": \"$EMAIL\"}"
# Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
# A "hold" answer also carries a reviewId (rev_...): keep it, the signup is resolved later by a
# webhook (signup.approved / signup.rejected) or by GET https://api.inlineshield.io/api/v1/reviews/<reviewId>.
# If the answer is {"success": false, "error": "NO_TOKEN"} (script blocked, timed out or
# missing), judge the signup server-side instead, with the visitor's IP and User-Agent
# taken from YOUR incoming request, and switch on "action" the same way
# (strict mode, REQUIRE_INLINESHIELD_TOKEN = true in the other snippets, rejects instead):
curl -X POST https://api.inlineshield.io/api/v1/verify \
-H "X-Api-Key: $INLINESHIELD_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$EMAIL\", \"ip\": \"$VISITOR_IP\", \"userAgent\": \"$VISITOR_UA\"}"
# HTTP 401 (bad API key) means reject. HTTP 402 {"error": "QUOTA_EXCEEDED", ...} means the
# account is over its monthly check limit: treat it like "InlineShield unavailable", exactly as
# for a timeout, 5xx, any other non-2xx answer, non-JSON, or a /verify answer with no
# "decision" field: accept the signup and log it (INLINESHIELD_FAIL_CLOSED = true in the other
# snippets rejects instead).<?php
define('INLINESHIELD_API_KEY', getenv('INLINESHIELD_API_KEY'));
require __DIR__ . '/inlineshield-guard.php';
const REQUIRE_INLINESHIELD_TOKEN = false; // true: reject signups that arrive without a token (NO_TOKEN). While your account is over its monthly limit, signups arrive without a token and InlineShield answers QUOTA_EXCEEDED, which follows INLINESHIELD_FAIL_CLOSED even in strict mode.
const INLINESHIELD_FAIL_CLOSED = false; // true: reject signups while InlineShield is unreachable
$failMode = INLINESHIELD_FAIL_CLOSED ? 'FAIL_CLOSED' : 'FAIL_OPEN';
// Reads $_POST['betshield_token'] and $_POST['email'], then calls
// https://api.inlineshield.io/api/v1/siteverify
// (JSON request body? pass ['token' => ..., 'email' => ...] in the options array)
$result = inlineshield_siteverify(null, ['failMode' => $failMode]);
// Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
// $reviewId is only set for "hold": store it with the pending account.
$reviewId = null;
if (($result['error'] ?? '') === 'NO_TOKEN' && !REQUIRE_INLINESHIELD_TOKEN) {
// No token (script blocked, timed out or missing): judge the signup server-side via
// https://api.inlineshield.io/api/v1/verify with the visitor's IP and User-Agent.
$guard = inlineshield_verify_registration(['email' => (string)($_POST['email'] ?? '')], ['failMode' => $failMode]);
// Quota exceeded, outages and other unexpected answers come back as the failMode result.
// A held signup comes back as REVIEW with its reviewId.
$action = match ($guard['decision'] ?? '') { 'BLOCK' => 'block', 'REVIEW' => 'hold', default => 'allow' };
$reviewId = $guard['reviewId'] ?? null;
} elseif (($result['error'] ?? '') === 'QUOTA_EXCEEDED') {
// Over the monthly limit: treated like "InlineShield unavailable" (follows INLINESHIELD_FAIL_CLOSED).
$action = INLINESHIELD_FAIL_CLOSED ? 'block' : 'allow';
} elseif (empty($result['success'])) {
$action = 'block';
} else {
// Fail-open answers have no "action": fall back to the decision.
$action = $result['action'] ?? match ($result['decision'] ?? '') { 'BLOCKED' => 'block', 'QUARANTINED' => 'hold', default => 'allow' };
$reviewId = $result['reviewId'] ?? null;
}
if ($action === 'block') {
http_response_code(403);
exit('Signup rejected');
}
if ($action === 'hold') {
// Create the account as pending (no login, no welcome email) and save $reviewId with it.
} else {
// "allow": create the account normally.
}// Node 18+ (global fetch). req.body.betshield_token is the form field.
// Calls https://api.inlineshield.io/api/v1/siteverify (and https://api.inlineshield.io/api/v1/verify when there is no token).
const REQUIRE_INLINESHIELD_TOKEN = false; // true: reject signups that arrive without a token (NO_TOKEN). While your account is over its monthly limit, signups arrive without a token and InlineShield answers QUOTA_EXCEEDED, which follows INLINESHIELD_FAIL_CLOSED even in strict mode.
const INLINESHIELD_FAIL_CLOSED = false; // true: reject signups while InlineShield is unreachable
// Returns the parsed JSON of a 2xx answer. Throws on transport error, timeout, any non-2xx
// (402 QUOTA_EXCEEDED, 429, 5xx, 400 ...) or non-JSON (= InlineShield unavailable).
// A 401 (bad API key) throws with .reject = true: always rejected.
async function inlineshieldPost(path, payload) {
const r = await fetch('https://api.inlineshield.io' + path, {
method: 'POST',
headers: { 'X-Api-Key': process.env.INLINESHIELD_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3000)
});
if (r.status === 401) throw Object.assign(new Error('InlineShield HTTP 401 (check INLINESHIELD_API_KEY)'), { reject: true });
if (r.status < 200 || r.status > 299) throw new Error('InlineShield HTTP ' + r.status);
const json = await r.json();
if (json && json.error === 'QUOTA_EXCEEDED') throw new Error('InlineShield QUOTA_EXCEEDED');
return json;
}
// Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
let action, reviewId = null; // reviewId is only set for "hold"
try {
let result = await inlineshieldPost('/api/v1/siteverify', { token: req.body.betshield_token, email: req.body.email });
if (result.error === 'NO_TOKEN' && !REQUIRE_INLINESHIELD_TOKEN) {
// No token (script blocked, timed out or missing): judge the signup server-side
// with the visitor's IP and User-Agent (set Express "trust proxy" behind a proxy).
result = await inlineshieldPost('/api/v1/verify', { email: req.body.email, ip: req.ip, userAgent: req.get('user-agent') || '' });
if (!result || !result.decision) throw new Error('InlineShield /verify answered without a decision');
} else if (result.success !== true) {
result = { action: 'block' };
}
// Fail-open answers have no "action": fall back to the decision.
action = result.action || (result.decision === 'BLOCKED' ? 'block' : result.decision === 'QUARANTINED' ? 'hold' : 'allow');
reviewId = result.reviewId || null;
} catch (err) {
if (err && err.reject) {
console.error('[InlineShield] rejecting signup:', err);
action = 'block';
} else {
console.warn('[InlineShield] unavailable, ' + (INLINESHIELD_FAIL_CLOSED ? 'rejecting' : 'accepting') + ' signup:', err);
action = INLINESHIELD_FAIL_CLOSED ? 'block' : 'allow';
}
}
if (action === 'block') {
return res.status(403).send('Signup rejected');
}
if (action === 'hold') {
// Create the account as pending (no login, no welcome email) and save reviewId with it.
} else {
// "allow": create the account normally.
}# Calls https://api.inlineshield.io/api/v1/siteverify (and https://api.inlineshield.io/api/v1/verify when there is no token).
import logging, os, requests
REQUIRE_INLINESHIELD_TOKEN = False # True: reject signups that arrive without a token (NO_TOKEN). While your account is over its monthly limit, signups arrive without a token and InlineShield answers QUOTA_EXCEEDED, which follows INLINESHIELD_FAIL_CLOSED even in strict mode.
INLINESHIELD_FAIL_CLOSED = False # True: reject signups while InlineShield is unreachable
class InlineShieldUnavailable(Exception):
pass
class InlineShieldRejected(Exception):
pass
def inlineshield_post(path, payload):
"""Returns the JSON of a 2xx answer. Raises InlineShieldUnavailable on transport error, timeout,
any non-2xx (402 QUOTA_EXCEEDED, 429, 5xx, 400 ...) or non-JSON, and InlineShieldRejected on 401."""
r = requests.post(
"https://api.inlineshield.io" + path,
headers={"X-Api-Key": os.environ["INLINESHIELD_API_KEY"]},
json=payload,
timeout=3,
)
if r.status_code == 401:
raise InlineShieldRejected("InlineShield HTTP 401 (check INLINESHIELD_API_KEY)")
if not 200 <= r.status_code <= 299:
raise InlineShieldUnavailable(f"InlineShield HTTP {r.status_code}")
data = r.json()
if isinstance(data, dict) and data.get("error") == "QUOTA_EXCEEDED":
raise InlineShieldUnavailable("InlineShield QUOTA_EXCEEDED")
return data
# In your signup view; token comes from the betshield_token form field.
# Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
email = request.form["email"]
review_id = None # only set for "hold"
try:
result = inlineshield_post("/api/v1/siteverify", {"token": request.form.get("betshield_token"), "email": email})
if result.get("error") == "NO_TOKEN" and not REQUIRE_INLINESHIELD_TOKEN:
# No token (script blocked, timed out or missing): judge the signup server-side
result = inlineshield_post("/api/v1/verify", {
"email": email,
"ip": request.remote_addr,
"userAgent": request.headers.get("User-Agent", ""),
})
if not result.get("decision"):
raise InlineShieldUnavailable("InlineShield /verify answered without a decision")
elif result.get("success") is not True:
result = {"action": "block"}
# Fail-open answers have no "action": fall back to the decision.
action = result.get("action") or {"BLOCKED": "block", "QUARANTINED": "hold"}.get(result.get("decision"), "allow")
review_id = result.get("reviewId")
except InlineShieldRejected as err:
logging.error("InlineShield rejected the request: %s", err)
action = "block"
except (requests.RequestException, ValueError, InlineShieldUnavailable, AttributeError) as err:
logging.warning("InlineShield unavailable: %s", err)
action = "block" if INLINESHIELD_FAIL_CLOSED else "allow"
if action == "block":
abort(403)
if action == "hold":
# Create the account as pending (no login, no welcome email) and save review_id with it.
pass
else:
# "allow": create the account normally.
pass// Java 11+. Minimal JSON handling without a library; use Jackson/Gson if you have one.
// Calls https://api.inlineshield.io/api/v1/siteverify (and https://api.inlineshield.io/api/v1/verify when there is no token).
static final HttpClient INLINESHIELD_HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
// Returns the body of a 2xx answer. Throws IOException on transport error, timeout, any non-2xx
// (402 QUOTA_EXCEEDED, 429, 5xx, 400 ...), non-JSON or QUOTA_EXCEEDED (= InlineShield unavailable),
// and SecurityException on 401 (bad API key: always rejected).
static String inlineshieldPost(String path, String body) throws IOException, InterruptedException {
HttpRequest vr = HttpRequest.newBuilder(URI.create("https://api.inlineshield.io" + path))
.timeout(Duration.ofSeconds(3))
.header("X-Api-Key", System.getenv("INLINESHIELD_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> r = INLINESHIELD_HTTP.send(vr, HttpResponse.BodyHandlers.ofString());
if (r.statusCode() == 401) throw new SecurityException("InlineShield HTTP 401 (check INLINESHIELD_API_KEY)");
if (r.statusCode() < 200 || r.statusCode() > 299) throw new IOException("InlineShield HTTP " + r.statusCode());
if (!r.body().trim().startsWith("{")) throw new IOException("InlineShield returned non-JSON");
if (r.body().matches("(?s).*\"error\"\\s*:\\s*\"QUOTA_EXCEEDED\".*")) throw new IOException("InlineShield QUOTA_EXCEEDED");
return r.body();
}
static String q(String v) {
return "\"" + (v == null ? "" : v.replace("\\", "\\\\").replace("\"", "\\\"")) + "\"";
}
// In your signup handler: inlineshieldToken is the betshield_token form field, email the
// submitted email, servletRequest the visitor's HttpServletRequest.
final boolean REQUIRE_INLINESHIELD_TOKEN = false; // true: reject signups that arrive without a token (NO_TOKEN). While your account is over its monthly limit, signups arrive without a token and InlineShield answers QUOTA_EXCEEDED, which follows INLINESHIELD_FAIL_CLOSED even in strict mode.
final boolean INLINESHIELD_FAIL_CLOSED = false; // true: reject signups while InlineShield is unreachable
// Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
String action;
String reviewId = null; // only set for "hold"
try {
String json = inlineshieldPost("/api/v1/siteverify", "{\"token\":" + q(inlineshieldToken) + ",\"email\":" + q(email) + "}");
if (json.matches("(?s).*\"error\"\\s*:\\s*\"NO_TOKEN\".*") && !REQUIRE_INLINESHIELD_TOKEN) {
// No token (script blocked, timed out or missing): judge the signup server-side
json = inlineshieldPost("/api/v1/verify", "{\"email\":" + q(email)
+ ",\"ip\":" + q(servletRequest.getRemoteAddr())
+ ",\"userAgent\":" + q(servletRequest.getHeader("User-Agent")) + "}");
if (!json.matches("(?s).*\"decision\"\\s*:.*")) throw new IOException("InlineShield /verify answered without a decision");
} else if (!json.matches("(?s).*\"success\"\\s*:\\s*true.*")) {
json = "{\"action\":\"block\"}";
}
// Fail-open answers have no "action": fall back to the decision.
java.util.regex.Matcher a = java.util.regex.Pattern.compile("\"action\"\\s*:\\s*\"(allow|hold|block)\"").matcher(json);
action = a.find() ? a.group(1) : (json.matches("(?s).*\"decision\"\\s*:\\s*\"BLOCKED\".*") ? "block"
: json.matches("(?s).*\"decision\"\\s*:\\s*\"QUARANTINED\".*") ? "hold" : "allow");
java.util.regex.Matcher r = java.util.regex.Pattern.compile("\"reviewId\"\\s*:\\s*\"(rev_[A-Za-z0-9_-]+)\"").matcher(json);
if (r.find()) reviewId = r.group(1);
} catch (SecurityException e) {
log.error("InlineShield rejected the request", e);
action = "block";
} catch (IOException | InterruptedException e) {
log.warn("InlineShield unavailable", e);
action = INLINESHIELD_FAIL_CLOSED ? "block" : "allow";
}
if (action.equals("block")) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Signup rejected");
}
if (action.equals("hold")) {
// Create the account as pending (no login, no welcome email) and save reviewId with it.
} else {
// "allow": create the account normally.
}// ASP.NET Core. inlineshieldToken is the betshield_token form field, httpContext the visitor's request.
// Calls https://api.inlineshield.io/api/v1/siteverify (and https://api.inlineshield.io/api/v1/verify when there is no token).
const bool REQUIRE_INLINESHIELD_TOKEN = false; // true: reject signups that arrive without a token (NO_TOKEN). While your account is over its monthly limit, signups arrive without a token and InlineShield answers QUOTA_EXCEEDED, which follows INLINESHIELD_FAIL_CLOSED even in strict mode.
const bool INLINESHIELD_FAIL_CLOSED = false; // true: reject signups while InlineShield is unreachable
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(3) }; // reuse one instance in production
// Returns the JSON of a 2xx answer. Throws HttpRequestException on transport error, timeout, any non-2xx
// (402 QUOTA_EXCEEDED, 429, 5xx, 400 ...) or QUOTA_EXCEEDED, JsonException on non-JSON
// (= InlineShield unavailable), and UnauthorizedAccessException on 401 (bad API key: always rejected).
async Task<JsonElement> InlineShieldPost(string path, object payload)
{
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.inlineshield.io" + path);
req.Headers.Add("X-Api-Key", Environment.GetEnvironmentVariable("INLINESHIELD_API_KEY"));
req.Content = JsonContent.Create(payload);
var resp = await http.SendAsync(req);
var code = (int)resp.StatusCode;
if (code == 401) throw new UnauthorizedAccessException("InlineShield HTTP 401 (check INLINESHIELD_API_KEY)");
if (code < 200 || code > 299) throw new HttpRequestException("InlineShield HTTP " + code);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
if (Str(json, "error") == "QUOTA_EXCEEDED") throw new HttpRequestException("InlineShield QUOTA_EXCEEDED");
return json;
}
static string? Str(JsonElement j, string name) =>
j.ValueKind == JsonValueKind.Object && j.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString() : null;
// Switch on action: "allow" → create the account; "hold" → create it pending and store reviewId; "block" → reject the signup.
string action;
string? reviewId = null; // only set for "hold"
try
{
var result = await InlineShieldPost("/api/v1/siteverify", new { token = inlineshieldToken, email = email });
if (Str(result, "error") == "NO_TOKEN" && !REQUIRE_INLINESHIELD_TOKEN)
{
// No token (script blocked, timed out or missing): judge the signup server-side
result = await InlineShieldPost("/api/v1/verify", new {
email = email,
ip = httpContext.Connection.RemoteIpAddress?.ToString(),
userAgent = httpContext.Request.Headers.UserAgent.ToString()
});
if (Str(result, "decision") == null) throw new HttpRequestException("InlineShield /verify answered without a decision");
}
else if (!(result.ValueKind == JsonValueKind.Object
&& result.TryGetProperty("success", out var ok) && ok.ValueKind == JsonValueKind.True))
{
result = JsonSerializer.SerializeToElement(new { action = "block" });
}
// Fail-open answers have no "action": fall back to the decision.
action = Str(result, "action") ?? (Str(result, "decision") switch { "BLOCKED" => "block", "QUARANTINED" => "hold", _ => "allow" });
reviewId = Str(result, "reviewId");
}
catch (UnauthorizedAccessException e)
{
logger.LogError(e, "InlineShield rejected the request");
action = "block";
}
catch (Exception e) when (e is HttpRequestException || e is TaskCanceledException || e is JsonException)
{
logger.LogWarning(e, "InlineShield unavailable");
action = INLINESHIELD_FAIL_CLOSED ? "block" : "allow";
}
if (action == "block")
return Results.StatusCode(403);
if (action == "hold")
{
// Create the account as pending (no login, no welcome email) and save reviewId with it.
}
else
{
// "allow": create the account normally.
}The PHP example loads InlineShield's PHP SDK files, which you download from the console's Setup page.
Option 2: server only, with /api/v1/verify
No script on the page? Call POST /api/v1/verify from your registration handler with the email, the visitor's IP address and their User-Agent, all taken from your own incoming request. Send the player's IP, not your server's: a cloud server's IP is a datacenter IP and would be held.
curl -X POST https://api.inlineshield.io/api/v1/verify \
-H "X-Api-Key: $INLINESHIELD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "ip": "203.0.113.10", "userAgent": "Mozilla/5.0 …"}'Reading the answer
decision: ALLOWED, QUARANTINED (held for review) or BLOCKED.action: allow, hold or block. A hold also carries areviewId.recommendedAction: allow, allow_no_bonus, review or block; thesignup.approvedwebhook can later say require_kyc. See risk verdicts.riskScore(0–100) andreasonsin plain language.
Held sign-ups are settled later by a signed webhook (signup.approved or signup.rejected) or by asking GET /api/v1/reviews/<reviewId>. Webhooks carry an HMAC-SHA256 signature in the X-InlineShield-Signature header. Changes to the API are listed in the integration changelog.
Frequently asked questions
Which languages have ready-made code?
PHP, Node.js, Python, Java and C#, plus cURL for anything else. The console's Setup page shows the same code with your keys filled in.
siteverify or verify: which should I use?
Use the browser script and /api/v1/siteverify when you can: the browser check adds signals a server can't see, such as headless browser hints and the time zone. Use /api/v1/verify when you can't change the sign-up page.
What if InlineShield is slow or down?
The snippets accept the sign-up and log a warning if InlineShield is unreachable, times out or errors. Set INLINESHIELD_FAIL_CLOSED to true to reject instead.
What happens when I go over my monthly quota?
/api/v1/verify answers HTTP 402 and /api/v1/siteverify answers success: false, both with QUOTA_EXCEEDED, and the snippets treat it like "InlineShield unavailable". You also get an email before you reach the limit.
Is there a sandbox?
There is no separate sandbox host yet. The console's Setup page has a "Send a test signup" button: it runs one sign-up through InlineShield with your own keys, creates a held test sign-up you can approve to try your webhook, and does not count against your quota.
How do I authenticate?
Server calls send your secret API key in the X-Api-Key header. The site key in the browser script is public and only identifies your site.
Get your API key
The Free plan includes 1,000 screenings a month. Create an account, add your keys and see real verdicts on your own traffic. No card, no sales call, no setup fee.