Integration changelog

Changes to the API and the integration kit that you can observe as an integrating developer. Newest first.

WordPress & WooCommerce plugin

  • New: the InlineShield Signup Protection plugin for WordPress, at https://api.inlineshield.io/downloads/inlineshield-wordpress.zip (also linked from https://inlineshield.io and from the console's Setup › WordPress tab). The response carries the plugin version in the X-Plugin-Version header.
  • It screens the WordPress registration form, WooCommerce My Account registration and, optionally, account creation at WooCommerce checkout. It adds the browser script to those forms, checks each sign-up with /api/v1/siteverify (or /api/v1/verify when there is no browser token) before the account is created, and refuses BLOCKED sign-ups. For QUARANTINED ones you choose: create and flag the account, create it but block login until you approve it in Review, or refuse it.
  • Install: Plugins › Add New › Upload Plugin, activate, then paste your API key and site key in Settings › InlineShield. To apply your review decisions, set your webhook URL (Settings › Webhook in the console) to https://<your-site>/wp-json/inlineshield/v1/webhook and paste the webhook signing secret into the plugin.
  • "Report fraud" and "Report legitimate" on the WordPress Users screen send outcome feedback (/api/v1/feedback). If InlineShield cannot be reached within 4 seconds, sign-ups are allowed (default) or refused, as you choose. Requires WordPress 6.0+ and PHP 7.4+; it uses the WordPress HTTP API, so the PHP curl extension is not needed.

Outcome feedback API and "Mark as fraud / legitimate"

  • New endpoint POST https://api.inlineshield.io/api/v1/feedback (X-Api-Key, every plan): tell InlineShield how a screened signup really turned out. Body: { requestId, outcome: "fraud" | "legit", category?, note? }. requestId is the requestId the API returned for the signup (/verify, /check, /siteverify); instead of it you can send token, the /check token. category is one of bonus_abuse, multi_account, chargeback, kyc_failed, bot, good_customer, other; note is free text of at most 200 characters and is never shown outside your account.
  • Responses: 200 { success: true, outcome, category, changed }. Sending the same feedback again changes nothing (changed: false); a different outcome replaces the earlier one. 404 NOT_FOUND for an unknown requestId or token. 422 SCREENING_TOO_OLD when the signup was screened more than 180 days ago. 400 BAD_REQUEST for a malformed body (send exactly one of requestId and token).
  • Feedback is not counted toward your monthly check limit and is accepted while you are over it. It is rate limited per API key with your plan's per-minute limit, in its own bucket, so feedback never uses up your /verify budget.
  • Use https://api.inlineshield.io. Like the rest of the server API, the endpoint also answers on the old host sec.iva.am for now, with the Deprecation: true and Link: <https://api.inlineshield.io>; rel="successor-version" headers, so an integration whose API base is still the old host can send feedback too.
  • PHP SDK: new method InlineShield\Client::feedback($requestId, $outcome, $options) (the BetShield\Client alias has it too); it returns { success, ... } or { success: false, error }. The console's Setup page has feedback snippets for PHP, Node, Python, Java, C# and curl.
  • Console: every History row (allowed ones included) has "Mark as fraud" and "Mark as legitimate", with Undo. Your marks and your feedback are used only to improve detection; they never train another operator's model.

BetShield is now InlineShield

  • BetShield is now called InlineShield and moves to inlineshield.io, one host per role: the website at https://inlineshield.io, the console at https://app.inlineshield.io, the integration API at https://api.inlineshield.io and the status page at https://status.inlineshield.io.
  • The API base is https://api.inlineshield.io (for example POST https://api.inlineshield.io/api/v1/siteverify). The browser script is https://api.inlineshield.io/inlineshield.js; /betshield.js is the same script under its old name and keeps working.
  • On sec.iva.am, pages redirect (301) to their inlineshield.io host. The API on sec.iva.am (/api/v1/verify, /evaluate, /check, /siteverify, /reviews/:id, /telemetry/beacon, /health, the PHP SDK downloads, /betshield.js and /inlineshield.js) keeps working for now, and its responses carry the headers Deprecation: true and Link: <https://api.inlineshield.io>; rel="successor-version". Please move to https://api.inlineshield.io: set your API base URL (INLINESHIELD_GATEWAY_URL or the SDK endpoint) to https://api.inlineshield.io and your script tag to https://api.inlineshield.io/inlineshield.js. A script still loaded from sec.iva.am already sends its checks to https://api.inlineshield.io, so allow that host in your Content-Security-Policy (connect-src).
  • Unchanged: the key prefixes (bs_live_, bs_site_, bs_sess_), the X-Api-Key header, every JSON field name, the betshield_token form field and the webhook payloads. Webhooks now carry X-InlineShield-Signature, X-InlineShield-Event and X-InlineShield-Delivery, and still carry the same X-BetShield-* headers with the same values, so an existing signature check keeps working. The webhook User-Agent is now InlineShield-Webhook-Dispatcher/2.4 (was BetShield-Webhook-Dispatcher/2.4); update any allowlist that matches it.
  • The old SDK names keep working. The PHP SDK is now InlineShield.php (namespace InlineShield, InlineShield\Client) with inlineshield-guard.php (inlineshield_verify_registration(), inlineshield_siteverify()); both files also define the old names (BetShield\Client, BetShield\BetShieldException, betshield_verify_registration(), betshield_siteverify()), so code written for BetShield.php and betshield-guard.php works with the new files, whichever name you save them under. Download both new files together; the new guard also works next to an old BetShield.php. The SDK default endpoint is https://api.inlineshield.io.
  • The PHP SDK and guard read INLINESHIELD_API_KEY, INLINESHIELD_GATEWAY_URL and INLINESHIELD_FAIL_MODE, and still read the BETSHIELD_* names when the new ones are not set. The generated snippets now use INLINESHIELD_API_KEY, INLINESHIELD_WEBHOOK_SECRET, REQUIRE_INLINESHIELD_TOKEN and INLINESHIELD_FAIL_CLOSED; rename the variables if you copy a new snippet over an old one.
  • The browser script sets window.InlineShield (window.BetShield is the same object) and fires inlineshield:incident as well as betshield:incident.

RANDOM_LOCAL_PART hold for random-looking addresses

  • New trigger RANDOM_LOCAL_PART: the part of the email address before the @ looks randomly generated, for example qmfu97kya77@, user7710525@, 7d5ba1a14919@ or zgqfzhihl@. A signup that would otherwise be ALLOWED is QUARANTINED for your review. It is never BLOCKED for this, and riskScore does not change. The reasons list says why in general terms and never quotes the address, for example "Email address looks randomly generated (no name or word; long mix of letters and digits)."
  • Unusual real names can occasionally be held; review and approve them. Addresses with a common name or word in them, keyboard patterns (asdfghjkl, 1q2w3e4r), repeated letters, leet spellings, phone numbers and other all-digit addresses are not held. A +tag is ignored. The check is a hint, not proof: an address that passes it is not thereby genuine.
  • The new "Random-looking addresses" switch in Settings > Rules (holdRandomLocalPart in the rules API, on by default) turns the hold off.
  • The check runs offline on our server, from a letter model and word list built from public word and name lists. No address or other data leaves the server for it.
  • emailAnalysis in /verify and sandbox responses has a new field, localPartRandomness: { random, score } (score 0 to 1, for information only), or null when the address is not a plain one. The public demo scan does not return it.
  • recommendedAction: signup.approved gives require_kyc also when the hold's triggers include RANDOM_LOCAL_PART.

SCRIPTED_HTTP_CLIENT trigger and datacenter holds

  • New trigger SCRIPTED_HTTP_CLIENT: the player's User-Agent is the default User-Agent of a scripted HTTP client (python-requests, aiohttp, Scrapy, curl, Wget, Go-http-client, Apache-HttpClient or PostmanRuntime), not a browser or an app. It adds 60 points, the same as HEADLESS_BROWSER_SIGNATURE, and is always on. A User-Agent that is also headless gets only HEADLESS_BROWSER_SIGNATURE. Only the client's own default form counts (for example "curl/8.5.0" at the start), so app User-Agents that mention a library (Unity's "libcurl/…", the legacy Android "Apache-HttpClient/UNAVAILABLE (java 1.4)"), native-app clients such as okhttp, Dart, CFNetwork, axios and node-fetch, and an empty or merely short User-Agent are not counted. botAnalysis has two new fields: scriptedUserAgent (true or false) and scriptedClient (the client's name, or null).
  • Datacenter IP addresses are now held: when DATACENTER_PROXY_IP fires, a signup that would otherwise be ALLOWED is QUARANTINED for your review. Its riskScore is unchanged (35 points). The new "Datacenter / cloud IPs" switch in Settings > Rules (blockDatacenterProxies in the rules API, on by default) turns both the points and the hold off. Always send the player's ip to /verify. Without it the request's own IP is used, which for a cloud-hosted backend is a datacenter IP and is now held.
  • POST /api/v1/verify reads the player's User-Agent only from the userAgent field of the request body. It no longer falls back to the request's own User-Agent header, which is your backend's HTTP client, not the player. Pass the visitor's User-Agent in userAgent, as all the SDK snippets do; without it the User-Agent is treated as empty and adds no points. POST /api/v1/telemetry/beacon does the same for calls authenticated with an API key or a console session; calls with a browser telemetry token still use the request's own User-Agent.
  • recommendedAction: signup.approved gives require_kyc also when the hold's triggers include DATACENTER_PROXY_IP or SCRIPTED_HTTP_CLIENT.

recommendedAction, new hold triggers and rule switches, and unknown dwell time in /verify

  • POST /api/v1/siteverify and POST /api/v1/verify responses have a new field, recommendedAction. It is sent next to action and does not replace it. Values: block (BLOCKED), review (QUARANTINED), allow_no_bonus (ALLOWED with riskScore 25 or higher) and allow (ALLOWED below 25).
  • The signup.approved and signup.rejected webhooks have the same field in data. signup.rejected gives block. signup.approved gives require_kyc when the hold's triggers include DUPLICATE_INBOX_ALIAS, VPN_IP, TIMEZONE_IP_MISMATCH or PLUS_ALIASED_EMAIL, and allow otherwise.
  • Four new triggers can hold a signup that would otherwise be ALLOWED (verdict QUARANTINED, no change to riskScore): DUPLICATE_INBOX_ALIAS (the same mailbox as an earlier signup at your site, for example a dot or plus variant), VPN_IP (the IP address is on a VPN provider list), TIMEZONE_IP_MISMATCH (the browser time zone is not used in the IP address's country) and NO_BROWSER_CHECK (the signup was screened without the browser check).
  • VPN_IP does not count Apple iCloud Private Relay: Apple's published Relay egress ranges are removed from the VPN list on every daily update.
  • Four rule switches in Settings > Rules and the rules API turn them on or off: holdDuplicateInbox, holdVpnIps and holdTimezoneMismatch default to true; holdWithoutBrowserCheck defaults to false. A signal that cannot be determined (no time zone, unknown IP country, no VPN data) never holds a signup and never adds points.
  • Webdriver or automation hints reported by the browser check count as a headless browser (HEADLESS_BROWSER_SIGNATURE).
  • POST /api/v1/verify no longer assumes a form dwell time of 4500 ms when none is sent. The dwell time is unknown: botAnalysis.dwellTimeMs is null, there is no "too fast" flag and no dwell points. A measured dwell time scores as before. The reasons list gets the neutral line "Screened without the browser check" for such signups.
  • The same applies to the telemetry beacon: a missing dwellTimeMs used to score as 0 ms (too fast, held) and is now unknown, so such calls can come back ALLOWED. Beacon and sandbox responses also get the "Screened without the browser check" line. If your minDwellTimeMs is above 4500, /verify calls that send no dwell time are no longer held for it.

Masked IP address on held and screened signups

  • GET /api/v1/reviews and GET /api/v1/screenings items have a new field, ipMasked: the network part of the player's IP address (IPv4 "46.71.x.x", IPv6 the first three groups followed by "…"), or null. The full address is never returned.
  • The console Review and History lists show it before the location, and the History CSV export has an "IP (masked)" column.

Billing by invoice: 14-day Pro trial, invoices, 7-day grace period and downgrade to Free

  • New self-serve signups that our screening allows start with a 14-day trial of the Pro plan. Other signups start on the Free plan.
  • Paid plans are billed by invoice, for 1, 3 or 12 months in advance. Invoices are payable within 14 days by bank transfer in USD. There are no card payments and nothing is charged automatically.
  • You can see your invoices, print them or save them as PDF, set your billing details and ask for an invoice in the console Usage section.
  • A plan is active from the moment InlineShield records your payment. Plans do not renew automatically: to renew, pay a new invoice. A renewal paid before the period ends keeps the original day of the month; a payment after the period has ended starts a new period from the payment day.
  • Screening continues on the Free plan. A trial moves to Free when it ends. After a paid period ends there is a 7-day grace period, and Enterprise clients get the same grace period. If no payment is recorded by then, the account moves to the Free plan (1,000 checks per month).
  • On a move to Free the month's usage count restarts, so the 1,000 Free checks apply from that moment. A plan set manually by InlineShield is not downgraded automatically.
  • You get email notices when a trial ends, before a paid period ends, when the grace period starts and when the account moves to Free.
  • Invoices are kept after an account is deleted, because they are financial records.

Disposable email detection from public lists and mail servers; real IP location and network data

  • emailAnalysis.disposableCategory has three new values: PUBLIC_DISPOSABLE_LIST (the email domain, or a parent domain, is on a public disposable-domain list), DISPOSABLE_MAIL_SERVER_HOST (the domain's mail server host sits under a listed disposable domain) and SHARED_DISPOSABLE_MAIL_SERVER (the domain's mail server IP address serves several listed disposable domains).
  • A signup matched through any of these is treated as a disposable email and scored the same as before (DISPOSABLE_TEMP_EMAIL). Domains on your allow list are never matched.
  • Country, city, ASN and network owner now come from a real IP location database, and Tor exit nodes from the public Tor exit list. Hosting and datacenter networks are recognised by their ASN.
  • When a value cannot be determined, the geo fields are null (an unknown country is null, not "INT") and the console shows "Unknown". An unknown value does not add to the risk score.
  • emailAnalysis.spf.status is now one of found, absent or unknown. The previous SPF qualifier moved to spf.qualifier.
  • If a domain's DNS lookup fails (SERVFAIL, REFUSED or a timeout), the missing MX, SPF and DMARC points are skipped for that signup. A null MX (RFC 7505) counts as no mail servers.
  • DNS lookups for one email domain finish within about 2 seconds.
  • The console shows the location and network owner of Held reviews.

Hold and approve, review APIs and webhooks, console rework

  • A signup can now come back with action "hold". Create the account as pending and store the returned reviewId. Held signups appear in the console Review section.
  • You get the decision through a signup.approved or signup.rejected webhook, or by GET /api/v1/reviews/<reviewId> with your server API key. The status endpoint is limited to 60 requests per minute per key.
  • signup.* webhook events are delivered to every enabled webhook endpoint, whatever event types it subscribes to.
  • Held reviews that nobody decides expire after 72 hours, and reminder digests are emailed before that.
  • The PHP guard and SDK return REVIEW (allowed: false) for a held signup. Download both files together, because an old BetShield.php with the new guard treats a held signup as a normal one.
  • Detection rules and the webhook URL are saved per account. A webhook signs each delivery with a secret that is specific to your account.
  • Webhook deliveries go only to the address that passed the safety check, and redirects are not followed.
  • Players' IP addresses sent in a request body are validated and trimmed. An invalid address gets HTTP 400.
  • The console has four sections (Setup, Review, Usage, Settings). Settings can change your password and sign out all devices.

Plans and monthly quotas

  • Each account has a plan with a monthly limit on /api/v1/check and /api/v1/verify calls. New accounts start on Free. GET /api/v1/plans lists the plans and GET /api/v1/usage returns your usage.
  • When the limit is reached, /check and /verify answer HTTP 402. Warning emails are sent when you pass 80% and 100% of the limit.
  • /siteverify is never counted or blocked. While your account is over its limit and the token is missing, it answers HTTP 200 with {"success": false, "error": "QUOTA_EXCEEDED"} instead of NO_TOKEN.
  • The generated server snippets and the PHP SDK follow the fail-closed switch (BETSHIELD_FAIL_CLOSED) for QUOTA_EXCEEDED, even when REQUIRE_BETSHIELD_TOKEN is on.
  • If the quota lookup itself fails, /check and /verify keep working and are not blocked.
  • Telemetry beacons count towards the monthly limit.

Integration kit: site keys, /check, /siteverify and betshield.js

  • Add betshield.js to your signup page with a public site key (data-sitekey). A site key is bound to your domains and a request from another origin is refused.
  • The script calls POST /api/v1/check, which screens the email and returns a single-use token. The script adds it to your form as betshield_token. For forms you submit with JavaScript, send form.betshield_token.value or new FormData(form).
  • Your backend sends the token to POST /api/v1/siteverify with a server API key and gets success, action and reasons. Switch on action: "allow" creates the account, "hold" creates it pending, "block" rejects it.
  • /check ignores any ip or userAgent in the request body. It uses the visitor's real IP address and User-Agent. It has its own rate limit per site key, and a 429 carries a Retry-After header.
  • A token can be redeemed once. A missing, used or expired token gives error "NO_TOKEN".
  • On NO_TOKEN the generated snippets screen the signup with POST /api/v1/verify, using the visitor IP and User-Agent. Set REQUIRE_BETSHIELD_TOKEN = true to reject those signups instead.
  • If InlineShield is unreachable, times out or returns an error, the snippets accept the signup and log a warning. Set BETSHIELD_FAIL_CLOSED = true to reject instead.
  • Server API keys are shown once when you create them and can be revoked in Settings.
  • Snippets are generated for curl, PHP, Node, Python, Java and C#. The PHP SDK has betshield_siteverify().