FraudGuard Integration Guide
Webhook integration reference for operators. Covers authentication, event formats, response handling, confidence scoring, graph cluster detection (ring fraud), billing, and optional device fingerprinting.
1. System Overview
FraudGuard is a multi-tenant SaaS platform that detects duplicate player accounts across online operators. When a player registers or makes a deposit, the operator sends a webhook event. FraudGuard normalises the player's identity fields, compares them against all existing players in the tenant's dataset, and returns a confidence score with a recommendation.
Optionally, an abuse detection module checks emails against disposable-email domain lists, IPs against VPN/proxy/Tor databases, and phones against a tenant-managed blacklist.
Operator System
│
│ POST /api/v1/webhook/{tenant_slug}/registration
│ POST /api/v1/webhook/{tenant_slug}/deposit
│ Header: X-Webhook-Secret: <secret>
▼
┌─────────────────────────────────────────┐
│ FraudGuard API │
│ │
│ 1. Authenticate tenant via secret │
│ 2. Billing: check quota gate │
│ 3. Normalise player fields │
│ 4. Run pairwise duplicate matching │
│ 5. (optional) Run abuse detection │
│ 6. Run graph cluster detection* │
│ 7. Persist alerts & usage record │
│ 8. Return JSON response │
└─────────────────────────────────────────┘
│
│ JSON: status, alerts[], abuse{}, clusters_found?
▼
Operator System handles recommendation
(ALLOW / REVIEW / FLAG / AUTO_BLOCK)
* auto-triggered for tenants with ≤200 players;
for larger tenants call POST /dashboard/clusters/detect
Key design principles
- Tenant isolation — player data from different operators never cross-compares.
- Idempotent upserts — sending the same player twice is safe; the record is updated, not duplicated.
- Normalisation-first — aliases, abbreviations, and formatting differences are collapsed before comparison.
- Non-blocking by default — the response always arrives; enforcement decisions belong to the operator.
2. Authentication
Each tenant receives a cryptographically random webhook secret when their
account is provisioned. Pass it in every webhook request as the
X-Webhook-Secret HTTP header. The secret is compared server-side and is the
only credential required for machine-to-machine webhook calls — no login or JWT is needed.
Rotating the secret
If the secret is compromised, contact your FraudGuard administrator to regenerate it. Update your operator system's secret value before the old one is invalidated to avoid a gap in coverage.
3. Webhook Integration
Step 1 — Obtain your credentials
Your tenant slug is betoperator and your webhook secret was provided
when your account was created. Both values appear in your dashboard under
Settings → Webhook.
Step 2 — Send a registration event
Fire this request whenever a new player completes registration on your platform:
curl -X POST https://fraudguard.example.com/api/v1/webhook/betoperator/registration \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_WEBHOOK_SECRET" \
-d '{
"player_id": "OPERATOR_12345",
"email": "[email protected]",
"first_name": "Jan",
"last_name": "Kowalski",
"date_of_birth": "1990-03-15",
"phone": "+48601234567",
"address": "ul. Kwiatowa 5/3 Warszawa",
"country": "PL",
"ip_address": "203.0.113.42"
}'
RegistrationEvent schema
{
"properties": {
"player_id": {
"title": "Player Id",
"type": "string"
},
"email": {
"title": "Email",
"type": "string"
},
"first_name": {
"title": "First Name",
"type": "string"
},
"last_name": {
"title": "Last Name",
"type": "string"
},
"date_of_birth": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Date Of Birth"
},
"phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Phone"
},
"address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Address"
},
"country": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Country"
},
"ip_address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Ip Address"
},
"fp": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Fp"
}
},
"required": [
"player_id",
"email",
"first_name",
"last_name"
],
"title": "RegistrationEvent",
"type": "object"
}
Step 3 — Send a deposit event
Fire this whenever a player makes a payment. Payment fingerprints (card, IBAN, crypto wallet) are the strongest duplicate signals — two accounts sharing a card number will almost always reach AUTO_BLOCK.
curl -X POST https://fraudguard.example.com/api/v1/webhook/betoperator/deposit \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_WEBHOOK_SECRET" \
-d '{
"player_id": "OPERATOR_12345",
"card_number": "4532 1234 5678 9010",
"iban": "PL61 1090 1014 0000 0712 1981 2874"
}'
DepositEvent schema
{
"properties": {
"player_id": {
"title": "Player Id",
"type": "string"
},
"card_number": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Card Number"
},
"iban": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Iban"
},
"crypto_wallet": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Crypto Wallet"
},
"email": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Email"
},
"first_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "First Name"
},
"last_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Last Name"
},
"date_of_birth": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Date Of Birth"
},
"phone": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Phone"
},
"address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Address"
},
"country": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Country"
},
"ip_address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Ip Address"
},
"fp": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"title": "Fp"
}
},
"required": [
"player_id"
],
"title": "DepositEvent",
"type": "object"
}
Step 4 — Handle the response
See Section 4 — Response Format for full response documentation.
PREPAID and your
duplicate-check quota is exhausted, the API returns 402 Payment Required
with {"error": "quota_exceeded", "detail": "..."}. Deposit and registration
webhooks will be blocked until your quota is renewed.
4. Response Format
Success response (200)
{
"status": "ok",
"player_id": "OPERATOR_12345",
"alerts_raised": 1,
"alerts": [
{
"alert_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"player_id_a": "OPERATOR_12345",
"player_id_b": "OPERATOR_00099",
"confidence": 0.8800,
"confidence_pct": "88%",
"recommendation": "FLAG",
"explanation": "Same DOB 1990-03-15 + last name (97%): Kowalski ↔ Kowalski; Normalized email match: [email protected]; Full name similarity 91%",
"signals": [
{"signal": "dob_lastname", "score": 0.985, "weight": 0.22, "contribution": 0.2167, "detail": "..."},
{"signal": "email_exact", "score": 1.0, "weight": 0.18, "contribution": 0.18, "detail": "..."},
{"signal": "name_full", "score": 0.91, "weight": 0.14, "contribution": 0.1274, "detail": "..."}
],
"status": "OPEN",
"created_at": "2024-03-15T10:30:00",
"resolved_at": null,
"operator_note": null
}
],
"abuse": {
"score": 0.75,
"recommendation": "FLAG",
"explanation": "VPN detected; Disposable email domain",
"signals": [
{"type": "vpn", "matched": true, "weight": 0.35},
{"type": "disposable_email", "matched": true, "weight": 0.40}
]
}
}
The "abuse" key is only present when abuse detection is enabled for your tenant and the abuse quota has not been exhausted.
Recommendation levels
| Recommendation | Confidence range | Suggested action |
|---|---|---|
| ALLOW | 0 – 0.44 | No significant match signals. Allow the player through normally. |
| REVIEW | 0.45 – 0.69 | Weak signals present. Queue for manual operator review. |
| FLAG | 0.70 – 0.89 | Likely duplicate. Flag account, block bonus eligibility, escalate. |
| AUTO_BLOCK | 0.90 – 1.00 | Near-certain duplicate. Block immediately without manual review. |
Alert statuses (set by your operators in the dashboard)
| Status | Meaning |
|---|---|
OPEN | New alert, not yet reviewed |
CONFIRMED | Operator confirmed as genuine duplicate |
DISMISSED | Operator dismissed — false positive |
BLOCKED | Account blocked (set on AUTO_BLOCK alerts automatically) |
5. Confidence Score Reference
The confidence score is the sum of signal contributions (score × weight), optionally boosted when multiple strong signals fire together or when a payment-instrument match would otherwise fall below the FLAG threshold.
Multi-signal boost: ≥3 signals with score ≥0.85 → ×1.15; ≥2 signals → ×1.08.
Payment floor: an exact card/IBAN/wallet match always raises confidence to at
least 0.7 (FLAG threshold).
| Signal | Description | Weight | Max contribution |
|---|---|---|---|
card | Payment card fingerprint (BIN6 + last 4 digits) | 0.30 | 30% |
iban | IBAN bank account number (exact match) | 0.28 | 28% |
device_fp | 0.26 | 26% | |
wallet | Cryptocurrency wallet address (exact match) | 0.25 | 25% |
dob_lastname | Same date of birth and similar last name (≥85% similarity) | 0.22 | 22% |
phone | Normalised phone number with country code (exact match) | 0.20 | 20% |
email_exact | Normalised email match (Gmail dots/plus-tags collapsed) | 0.18 | 18% |
name_full | Full name token-sort similarity (≥75%) | 0.14 | 14% |
email_fuzzy | Email string similarity ≥80% (catches typo variants) | 0.12 | 12% |
address | Address token-sort similarity after abbreviation expansion (≥70%) | 0.10 | 10% |
dob | Same date of birth alone (no last name signal present) | 0.08 | 8% |
country | Same country code | 0.03 | 3% |
| Threshold constant | Value | Meaning |
|---|---|---|
THRESHOLD_REVIEW | 0.45 | Worth a human look |
THRESHOLD_FLAG | 0.7 | Likely duplicate — FLAG recommendation |
THRESHOLD_AUTO_BLOCK | 0.9 | Near-certain duplicate — AUTO_BLOCK recommendation |
6. Graph Cluster Detection (Ring Fraud)
Pair-level duplicate checks catch two accounts that share a signal. Ring fraud uses chains of intermediary accounts — A links to B, B links to C — so no single pair triggers a high-confidence alert. FraudGuard models every account as a node in an identity graph and finds connected components (clusters) using a Union-Find algorithm. A cluster with three or more members and high inter-connection density is a strong indicator of an organised fraud ring.
Auto-trigger behaviour
After every webhook call, FraudGuard checks whether the tenant has ≤ 200 players.
If so, cluster detection runs automatically and the result is included in the webhook response
as clusters_found. For larger tenants the field is omitted and you should call
POST /dashboard/clusters/detect from a scheduled background job instead.
Cluster confidence formula
Each cluster is assigned a cluster_conf score in [0, 1]:
avg_conf = average pair-wise confidence of all edges in the cluster size_bonus = 0.05 × min(member_count − 2, 8) # capped at +0.40 density_bonus = 0.10 × edge_density # edge_density ∈ [0, 1] cluster_conf = min(1.0, avg_conf + size_bonus + density_bonus)
A cluster with 6 members and 70 % edge density and an average pair confidence of 0.75
would score min(1.0, 0.75 + 0.20 + 0.07) = 0.97.
The recommendation field maps the score to REVIEW (<0.65),
FLAG (<0.80), or BLOCK (≥0.80).
Webhook response with clusters
POST /api/v1/webhook/<tenant_slug>/registration
→ 200 OK
{
"status": "ok",
"player_id": "OPERATOR_12345",
"duplicates_found": 1,
"alerts": [ ... ],
"clusters_found": 1
}
Manual detection — POST /dashboard/clusters/detect
Rebuilds the entire identity graph for the authenticated tenant and persists any new or updated clusters. Use this endpoint for tenants with more than 200 players, or to force a re-run after bulk imports.
POST /dashboard/clusters/detect
Authorization: Bearer <dashboard_token>
Content-Type: application/json
{}
→ 200 OK
{
"clusters_found": 4,
"message": "Detection complete. 4 cluster(s) found."
}
Listing clusters — GET /dashboard/clusters
Returns all clusters for the tenant, newest first. Supports pagination and status filtering.
| Query param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: OPEN, CONFIRMED, or DISMISSED. Omit for all. |
page | integer | 1 | Page number (1-based). |
page_size | integer | 20 | Results per page (max 100). |
GET /dashboard/clusters?status=OPEN&page=1&page_size=5
Authorization: Bearer <dashboard_token>
→ 200 OK
{
"items": [
{
"id": 1,
"member_count": 5,
"avg_conf": 0.81,
"cluster_conf": 0.97,
"status": "OPEN",
"recommendation": "BLOCK",
"created_at": "2025-11-14T09:22:11Z"
}
],
"total": 1,
"page": 1,
"page_size": 5
}
Cluster detail — GET /dashboard/clusters/{id}
GET /dashboard/clusters/1
Authorization: Bearer <dashboard_token>
→ 200 OK
{
"id": 1,
"member_count": 5,
"ext_ids": ["OP_001", "OP_002", "OP_003", "OP_004", "OP_005"],
"names": ["Alice K.", "Alice K.", "A. Kowalski", "A.K.", "Alice"],
"avg_conf": 0.81,
"cluster_conf": 0.97,
"status": "OPEN",
"recommendation": "BLOCK",
"explanation": "5 accounts share phone suffix, device fingerprint and IP subnet. High-density ring (8/10 possible edges present).",
"operator_note": null,
"created_at": "2025-11-14T09:22:11Z",
"updated_at": "2025-11-14T09:22:11Z"
}
Resolving a cluster — POST /dashboard/clusters/{id}/resolve
Mark a cluster as CONFIRMED (fraud confirmed — use to trigger downstream
bans or review workflows) or DISMISSED (false positive — suppresses it from
the default view). An optional operator note can be attached for audit purposes.
POST /dashboard/clusters/1/resolve
Authorization: Bearer <dashboard_token>
Content-Type: application/json
{
"status": "CONFIRMED",
"note": "Verified ring — all five accounts share the same device and phone."
}
→ 200 OK
{
"id": 1,
"status": "CONFIRMED",
"note": "Verified ring — all five accounts share the same device and phone."
}
Dashboard UI
The Ring Clusters page in the operator dashboard lists all clusters with
status filters (All / Open / Confirmed / Dismissed), confidence bars, and a detail modal that
shows member external IDs, shared signal explanation, and the Confirm / Dismiss action buttons.
Running detection manually from the UI calls POST /dashboard/clusters/detect.
7. Billing
Plan types
| Type | How it works | Quota behaviour |
|---|---|---|
payg |
Every check is billed at price_per_check for each service. |
No quota — checks are never blocked. |
prepaid |
Fixed credit pool purchased upfront. Credits are consumed per check. | Hard block when credits_used >= prepaid_credits; returns 402. |
flexible_prepaid |
Fixed credit pool; once exhausted, additional checks are billed at the overage rate. | Never hard-blocked; overage price applies above quota. |
Quota exhaustion — 402 response
When a PREPAID tenant's duplicate quota is exhausted, webhook calls return:
HTTP/1.1 402 Payment Required
{
"error": "quota_exceeded",
"detail": "Duplicate check quota exceeded"
}
If only the abuse quota is exhausted, the duplicate check still runs and only the abuse portion is silently skipped for that call.
Usage records
Every webhook call (including blocked ones) creates a usage record visible in the dashboard under Billing → Records. Costs are zero for in-budget prepaid checks; non-zero only for PAYG or flexible-prepaid overage.
8. Device Fingerprinting (optional)
FraudGuard provides an optional JavaScript library — fraudguard-fp.js — that
operators can embed in their registration page. When installed, it collects two categories
of signals and merges them into the webhook payload:
- Device fingerprint — a stable hash of browser + hardware characteristics (GPU renderer, canvas rendering, installed codecs, screen geometry, timezone, platform). Two registrations from the same physical device produce the same hash even when personal data (email, name) differs completely.
- Behavioral signals — anonymised statistics about how the form was filled: mouse movement entropy, inter-keystroke timing (mean + std dev), inter-click timing, field fill order, and time spent per field. Bot-driven form fills differ sharply from human patterns.
When FraudGuard receives a fingerprint, the device hash is treated as an additional duplicate-detection signal with weight 0.26. A shared device hash between two player accounts is a strong fraud indicator — comparable in weight to a shared payment card — and will typically raise the confidence score above the FLAG threshold.
Installation
Add the script tag to your registration page, before the closing </body>:
<script src="https://<your-fraudguard-host>/static/js/fraudguard-fp.js"></script>
Then attach it to your registration form and merge the payload on submit:
// 1. Attach after your form is in the DOM
const form = document.getElementById('registration-form');
FraudGuard.attach(form);
// 2. On submit, spread the fingerprint payload into your POST body
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
player_id: form.player_id.value,
email: form.email.value,
first_name: form.first_name.value,
last_name: form.last_name.value,
date_of_birth: form.dob.value,
phone: form.phone.value,
...FraudGuard.getPayload(), // adds the "fp" key with device + behavioral data
};
await fetch('/api/v1/webhook/{slug}/registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Webhook-Secret': 'YOUR_SECRET' },
body: JSON.stringify(formData),
});
});
FraudGuard.getPayload() returns a single top-level key fp
containing the device hash (fp.dh), behavioral stats (fp.bh),
and raw browser attributes (fp.bf). The entire object is encrypted at rest.
What is collected
| Field | Key | Description |
|---|---|---|
| Device hash | fp.dh | 8-char hex hash of stable browser+hardware characteristics |
| Mouse entropy | fp.bh.mm_e | Variance of mouse movement speed (0.0–1.0). Low values indicate bots. |
| Keystroke timing | fp.bh.kt_m, fp.bh.kt_sd | Mean and std dev of inter-keystroke intervals (ms) |
| Click timing | fp.bh.ct_m, fp.bh.ct_sd | Mean and std dev of inter-click intervals (ms) |
| Field fill order | fp.bh.fo | Comma-separated field names in order they were first focused |
| Field durations | fp.bh.fdt | Milliseconds spent focused on each field |
| Per-field keystroke stats | fp.bh.fkt | Mean + std dev of keystroke timing per individual field |
| Session duration | fp.bh.dur | Total milliseconds from script load to form submit |
| User agent | fp.bf.ua | Browser user-agent string |
| GPU renderer | fp.bf.wgl | WebGL renderer string (identifies GPU + driver) |
| Canvas fingerprint | fp.bf.cfp | Hash of sub-pixel canvas rendering (varies by OS/GPU/font config) |
| Codecs | fp.bf.cv | Supported media codecs (comma-separated) |
| Screen | fp.bf.sw, fp.bf.sh, fp.bf.cd | Screen width, height, colour depth |
Privacy considerations
No personally identifiable information is sent beyond what is already in the registration form. Behavioral data is aggregated (means and standard deviations only — raw keystroke sequences are not stored). All fingerprint data is encrypted at rest using the same AES-128 encryption as player PII fields.
The library is entirely passive — it listens for mouse and keyboard events but never reads field values, never intercepts passwords, and never modifies the DOM.