Docs menu
SDKs

Python SDK

The official Python package, inlineshield: screen sign-ups, redeem browser tokens, send feedback, poll held sign-ups and verify webhooks, with the standard library only.

Last updated

Install

inlineshield 1.0.0 needs Python 3.9 or later and uses only the standard library (urllib, hmac, json). It is typed (py.typed). Until it is published on PyPI, install it from our download, the wheel inlineshield-1.0.0-py3-none-any.whl or the source distribution inlineshield-1.0.0.tar.gz:

Shell
pip install https://inlineshield.io/downloads/sdk/inlineshield-1.0.0-py3-none-any.whl
# or the source distribution: pip install https://inlineshield.io/downloads/sdk/inlineshield-1.0.0.tar.gz

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).

Python
import os
from inlineshield import Client

# Server-side only: your secret key (bs_live_… or bs_test_…) from the console, Settings › Keys.
shield = Client(
    os.environ["INLINESHIELD_API_KEY"],
    timeout_ms=3000,  # per request (the default)
    fail_open=True,   # the default: an outage gives an "unavailable" result with action "allow"
)
OptionDefaultWhat it does
base_urlhttps://api.inlineshield.ioThe API origin. INLINESHIELD_BASE_URL from the environment, if set.
timeout_ms3000Deadline of each request, in milliseconds: connecting, sending and reading the whole answer.
fail_openTrueReturn an unavailable result (action "allow") instead of raising when InlineShield is unavailable.
max_retries1Retries after a failed connection (nothing was sent).

The base URL must use https; plain http is accepted only for localhost, for local tests.

Methods

Results are the API's JSON as dicts, with the API's field names (see the API reference), plus unavailable: false for a real answer, true for a fail-open result.

MethodWhat it does
verify(email, *, ip, user_agent, username, client_dwell_ms, honeypot)POST /api/v1/verify: screen a sign-up on the server.
siteverify(token, email)POST /api/v1/siteverify: redeem the browser script token.
feedback(request_id | {"token": ...}, outcome, *, category, note)POST /api/v1/feedback: report fraud or legit.
get_review(review_id)GET /api/v1/reviews/{reviewId}: the status of a held sign-up.
verify_webhook(signature_header, raw_body, secret, tolerance_sec=300)Check a webhook delivery and return its event. Also Client.verify_webhook.

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.

Check 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).
Python
import os
from flask import request
from inlineshield import Client

shield = Client(os.environ["INLINESHIELD_API_KEY"])


def screen_signup():
    """In your sign-up view (Flask): the browser script put the token in the form."""
    email = request.form["email"]
    checked = shield.siteverify(request.form.get("betshield_token"), email)
    if checked["success"] or checked["unavailable"]:
        return checked
    if checked["error"] == "NO_TOKEN":
        # The script was blocked, timed out or is missing: screen on the server instead.
        return shield.verify(email, ip=request.remote_addr, user_agent=request.headers.get("User-Agent", ""))
    return {"action": "block", "error": checked["error"]}  # INVALID_TOKEN, EXPIRED_TOKEN, ...


result = screen_signup()
if result["action"] == "allow":
    pass  # create the account
elif result["action"] == "hold":
    pass  # create it pending and keep result.get("reviewId") (live holds)
else:
    pass  # reject the sign-up

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.

Python
import os
from flask import request
from inlineshield import Client

shield = Client(os.environ["INLINESHIELD_API_KEY"])

result = shield.verify(
    request.form["email"],
    ip=request.remote_addr,                            # the player's IP, not your server's
    user_agent=request.headers.get("User-Agent", ""),  # the player's browser
    username=request.form.get("username"),             # optional
)
# result["action"]: "allow", "hold" or "block"; result["riskScore"] 0-100; result["reasons"]
if result["unavailable"]:
    pass  # 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.

Python
import os
from inlineshield import Client

shield = Client(os.environ["INLINESHIELD_API_KEY"])

# By the requestId InlineShield returned (accepted for screenings up to 180 days old):
sent = shield.feedback(request_id, "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.

Python
import os
from inlineshield import Client

shield = Client(os.environ["INLINESHIELD_API_KEY"])

# The reviewId of a held sign-up. Webhooks tell you first; this is the fallback.
review = shield.get_review(review_id)
if review["status"] == "approved":
    pass  # activate the account
elif review["status"] == "rejected":
    pass  # 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. With Flask, request.get_data() gives the raw bytes. See Webhooks.

Python
import os
from flask import abort, request
from inlineshield import WebhookVerificationError, verify_webhook

# app is your Flask application: app = Flask(__name__)


@app.post("/inlineshield/webhook")
def inlineshield_webhook():
    try:
        event = verify_webhook(
            request.headers.get("X-InlineShield-Signature"),
            request.get_data(),  # the raw body bytes, before any JSON parsing
            os.environ.get("INLINESHIELD_WEBHOOK_SECRET"),
        )
    except WebhookVerificationError:
        abort(400)
    if event["event"] == "signup.approved":
        pass  # activate the account that has reviewId event["data"]["reviewId"]
    elif event["event"] == "signup.rejected":
        pass  # delete or disable it (event["data"]["decisionReason"])
    # Deliveries can repeat: make this idempotent (key on event["id"]).
    return "", 200
Error codeMeaning
NO_SECRETThe secret is empty or missing: the SDK never verifies with an empty key.
MALFORMED_HEADERThe header is not exactly t=<seconds>,v1=<64 lower-case hex characters>.
TIMESTAMP_OUT_OF_RANGEThe delivery was signed more than the tolerance (300 seconds by default) before or after now.
SIGNATURE_MISMATCHThe HMAC does not match: another secret, or a changed body.
INVALID_PAYLOADThe 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 fail_open 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 errorWhen
TIMEOUTNo answer within the timeout.
UNAVAILABLENo connection, a server error (5xx), or an answer that is not JSON.
RATE_LIMIT_EXCEEDEDToo many requests (429).
TEST_LIMITA test key is over its daily test limit (429).
QUOTA_EXCEEDEDThe account is over its monthly quota (402, or QUOTA_EXCEEDED from siteverify).
A rate limit or a quota block also fails open. By default a 429 (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 fail_open to false and handle the rate-limit and quota errors yourself.
Error classWhen
AuthErrorA missing, unknown, revoked or expired key (401), or a suspended account (403 TENANT_SUSPENDED). Always raised.
ApiErrorA refused request: 400 BAD_REQUEST or INVALID_IP, 404 NOT_FOUND, 422 SCREENING_TOO_OLD, or a redirect (UNEXPECTED_REDIRECT). Always raised.
UnavailableErrorCode TIMEOUT or UNAVAILABLE. Raised only with fail_open=False.
RateLimitErrorCode RATE_LIMIT_EXCEEDED or TEST_LIMIT, with retry_after in seconds. Raised only with fail_open=False.
QuotaErrorCode QUOTA_EXCEEDED. Raised only with fail_open=False.
WebhookVerificationErrorRaised by verify_webhook.

Every class extends InlineShieldError, with code (the error code) and status (the HTTP status, or None when there was no answer). 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.

Python
import os
from flask import request
from inlineshield import ApiError, AuthError, Client, QuotaError, RateLimitError, UnavailableError

shield = Client(os.environ["INLINESHIELD_API_KEY"])

try:
    result = shield.verify(request.form["email"], ip=request.remote_addr,
                           user_agent=request.headers.get("User-Agent", ""))
    if result["unavailable"]:
        pass  # fail-open: result["error"] is TIMEOUT, UNAVAILABLE, RATE_LIMIT_EXCEEDED, TEST_LIMIT or QUOTA_EXCEEDED
except AuthError as err:
    # INVALID_API_KEY, EXPIRED_API_KEY, TENANT_SUSPENDED...: fix the configuration (raised even with fail_open).
    raise RuntimeError("InlineShield is not configured: " + err.code) from err
except ApiError as err:
    # The request was refused: err.code (e.g. INVALID_IP) and err.status.
    raise

# Fail-closed instead: with fail_open=False an outage raises.
strict = Client(os.environ["INLINESHIELD_API_KEY"], fail_open=False)
try:
    strict.verify(request.form["email"], ip=request.remote_addr, user_agent=request.headers.get("User-Agent", ""))
except (UnavailableError, RateLimitError, QuotaError):
    pass  # InlineShield is unavailable: reject the sign-up, or ask the player to try again (retry_after on a rate limit)