← back

Integrate fraud detection

Three ways to wire this into a product that already has its own signup and login pages. Pick the one that matches how much control you need over the decision.

PatternIntegration effortSecurityWhen to use
A · Drop-in JS1 line of HTMLlow – decision returned client-sideMVP, marketing signups, demos
B · Server-sideJS collect + backend forwardhigh – decision computed server-sideproduction, payments, anything money-shaped
C · Hybrid token1 line of HTML + 1 backend callhigh – browser-collected, backend-verifiedbest of both, lowest friction

Pattern A · Drop-in JS (zero backend changes)

Add one <script> tag to your signup page. The SDK hooks your existing form's submit event, collects device + behavioural signals, calls the fraud API, and either lets the submit proceed or stops it based on the decision. No changes to your backend.

Declarative — pure HTML

signup.htmlhtml
<form id="signup-form" action="/your-existing-signup-endpoint" method="POST">
  <input type="email" name="email" required />
  <input type="password" name="password" required />
  <button type="submit">Create account</button>
</form>

<script src="https://detect.mandet.co/fraud-detect.js"
        data-tenant="demo"
        data-signup-form="#signup-form"
        data-on-block="alert('Suspicious activity, please contact support.'); return false;"
        data-on-challenge="window.location='/verify-email?eventId='+result.eventId; return false;"></script>

Programmatic — full control

signup.html (advanced)html
<script src="https://detect.mandet.co/fraud-detect.js"></script>
<script>
  FraudDetect.init({ tenant: 'demo' });

  FraudDetect.attachToForm(document.querySelector('#signup-form'), {
    type: 'SIGNUP',

    onResult(result) {
      // Tag the visitor/session in your analytics — useful for funnel correlation
      window.dataLayer && window.dataLayer.push({ event: 'fraud_score', score: result.riskScore });
    },

    onAllow(result)     { return true; },                       // let the form submit
    onChallenge(result) { showEmailVerificationModal(result.eventId); return false; },
    onBlock(result)     { showErrorBanner('Account creation blocked.'); return false; },

    onError(err) { console.error('fraud sdk failed', err); /* fail-open by default */ },
  });
</script>

The SDK runs cross-origin from any domain. It returns decision (ALLOW / CHALLENGE / BLOCK), riskScore, and the full triggeredRules list. A simulated example of every response shape is shown on the demo page.

Trade-off. The decision is computed in the browser's network round-trip — a determined attacker can intercept the response and force a different decision client-side. For low-stakes signups this is fine. For high-stakes flows (financial accounts, anything that grants money), use Pattern B or C.

Pattern B · Server-side check (recommended for production)

Same browser-side data collection, but the decision is made in your backend with a secret API key. Browsers see only the signals they emit; your backend talks to ours.

1. Browser — collect & forward to your backend

signup.htmlhtml
<script src="https://detect.mandet.co/fraud-detect.js"></script>
<script>
  document.querySelector('#signup-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const signals = await FraudDetect.collectDevice();   // returns the device payload only
    // POST to YOUR own signup endpoint, including the signals
    await fetch('/api/signup', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: e.target.email.value,
        password: e.target.password.value,
        fraudSignals: signals,
      }),
    });
  });
</script>

2. Your backend — call our API with your secret key

routes/signup.ts (Node)ts
const FRAUD_KEY = process.env.FRAUD_API_KEY!;

app.post('/api/signup', async (req, res) => {
  const { email, password, fraudSignals } = req.body;

  const fraud = await fetch('https://detect.mandet.co/api/v1/events/ingest', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-api-key': FRAUD_KEY },
    body: JSON.stringify({
      type: 'SIGNUP',
      email,
      externalUserId: email,    // or your internal user id
      ip: req.ip,               // important: pass the real client IP
      device: fraudSignals,
    }),
  }).then(r => r.json());

  if (fraud.decision === 'BLOCK')     return res.status(403).json({ error: 'blocked' });
  if (fraud.decision === 'CHALLENGE') return res.json({ challenge: 'email_verification', eventId: fraud.eventId });
  const user = await createUser({ email, password });
  return res.json({ user, eventId: fraud.eventId });
});

The API key is created in /client-admin/api-keys (one click). It's shown once; we only store a SHA-256 hash. Revoke instantly from the same page.

Pattern C · Hybrid with verification token (Stripe-/reCAPTCHA-style)

The SDK calls our API, but the decision is bound to a short-lived event id that your backend redeems. You get the security of Pattern B with the integration speed of Pattern A.

1. Browser — same as Pattern A, but send the event id to your backend

signup.htmlhtml
<script src="https://detect.mandet.co/fraud-detect.js"></script>
<script>
  FraudDetect.init({ tenant: 'demo' });
  FraudDetect.attachToForm(document.querySelector('#signup-form'), {
    type: 'SIGNUP',
    onResult(result) {
      // Stash the event id on the form so the browser submits it to your backend
      const hidden = document.createElement('input');
      hidden.type = 'hidden';
      hidden.name = 'fraudEventId';
      hidden.value = result.eventId;
      document.querySelector('#signup-form').appendChild(hidden);
    },
    onAllow:     ()  => true,
    onChallenge: ()  => true,   // let the form submit; let your backend decide
    onBlock:     ()  => true,
  });
</script>

2. Your backend — verify the event id with your secret key

routes/signup.tsts
app.post('/api/signup', async (req, res) => {
  const { email, password, fraudEventId } = req.body;

  // Fetch the canonical decision from us (can't be forged by the browser)
  const event = await fetch(`https://detect.mandet.co/api/v1/events/${fraudEventId}`, {
    headers: { 'x-api-key': process.env.FRAUD_API_KEY! },
  }).then(r => r.json());

  if (event.decision !== 'ALLOW') return res.status(403).json({ error: event.decision });
  const user = await createUser({ email, password });
  return res.json({ user });
});

The browser still hits the public SDK (Pattern A behaviour) but your backend reads the canonical stored decision using its server-side API key. An attacker intercepting the SDK response can't fake a different decision because your backend re-fetches the truth.


Reference

Response shape (Pattern A and B both return this)

responsejson
{
  "eventId": "cmp4...",
  "riskScore": 85,
  "riskBand": "MONITOR",           // ALLOW | MONITOR | BLOCK
  "decision": "CHALLENGE",         // ALLOW | CHALLENGE | BLOCK
  "triggeredRules": [
    { "code": "ACCOUNTS_PER_EMAIL_EXCEEDED", "label": "Same email reused on multiple accounts", "weight": 45 },
    { "code": "DISPOSABLE_EMAIL", "label": "Disposable email", "weight": 30 }
  ],
  "allChecks":      [ /* all 18 rules, FIRED | PASS | DISABLED */ ],
  "derivedSignals": { /* IP geo, device, behaviour, linkage counts, … */ }
}

Per-tenant tuning

Each tenant tunes thresholds and per-rule weights in /client-admin/rules. The defaults (ALLOW < 80 < MONITOR < 120 < BLOCK) work well for an e-commerce signup; tighten them for higher-stakes flows.

FingerprintJS open-source caveat

The SDK uses FingerprintJS OSS for the clientFingerprint. Its visitorId can drift across incognito, private modes, and some browser updates — meaning the same human may appear as different devices. The system compensates by weighting IP, ASN, email, and behavioural signals — multi-signal detection still catches fraud rings — but for cross-browser device identity, upgrade to FingerprintJS Pro: only the SDK's init call changes.

Security checklist


Sandbox

A bare-bones HTML page that demonstrates Pattern A end-to-end, including the live API response: /sandbox.html — open the network tab to see the SDK's actual request.