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

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.

The webhook secret is shown once at tenant creation. Store it in your secrets manager immediately — it cannot be retrieved later, only regenerated by a superadmin.

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.

Quota enforcement: If your plan is 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

RecommendationConfidence rangeSuggested 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)

StatusMeaning
OPEN New alert, not yet reviewed
CONFIRMEDOperator confirmed as genuine duplicate
DISMISSEDOperator 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).

SignalDescriptionWeightMax contribution
cardPayment card fingerprint (BIN6 + last 4 digits)0.3030%
ibanIBAN bank account number (exact match)0.2828%
device_fp0.2626%
walletCryptocurrency wallet address (exact match)0.2525%
dob_lastnameSame date of birth and similar last name (≥85% similarity)0.2222%
phoneNormalised phone number with country code (exact match)0.2020%
email_exactNormalised email match (Gmail dots/plus-tags collapsed)0.1818%
name_fullFull name token-sort similarity (≥75%)0.1414%
email_fuzzyEmail string similarity ≥80% (catches typo variants)0.1212%
addressAddress token-sort similarity after abbreviation expansion (≥70%)0.1010%
dobSame date of birth alone (no last name signal present)0.088%
countrySame country code0.033%
Threshold constantValueMeaning
THRESHOLD_REVIEW 0.45Worth 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 paramTypeDefaultDescription
statusstringFilter by status: OPEN, CONFIRMED, or DISMISSED. Omit for all.
pageinteger1Page number (1-based).
page_sizeinteger20Results 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

TypeHow it worksQuota 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:

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

FieldKeyDescription
Device hashfp.dh8-char hex hash of stable browser+hardware characteristics
Mouse entropyfp.bh.mm_eVariance of mouse movement speed (0.0–1.0). Low values indicate bots.
Keystroke timingfp.bh.kt_m, fp.bh.kt_sdMean and std dev of inter-keystroke intervals (ms)
Click timingfp.bh.ct_m, fp.bh.ct_sdMean and std dev of inter-click intervals (ms)
Field fill orderfp.bh.foComma-separated field names in order they were first focused
Field durationsfp.bh.fdtMilliseconds spent focused on each field
Per-field keystroke statsfp.bh.fktMean + std dev of keystroke timing per individual field
Session durationfp.bh.durTotal milliseconds from script load to form submit
User agentfp.bf.uaBrowser user-agent string
GPU rendererfp.bf.wglWebGL renderer string (identifies GPU + driver)
Canvas fingerprintfp.bf.cfpHash of sub-pixel canvas rendering (varies by OS/GPU/font config)
Codecsfp.bf.cvSupported media codecs (comma-separated)
Screenfp.bf.sw, fp.bf.sh, fp.bf.cdScreen 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.