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.
| Pattern | Integration effort | Security | When to use |
|---|---|---|---|
| A · Drop-in JS | 1 line of HTML | low – decision returned client-side | MVP, marketing signups, demos |
| B · Server-side | JS collect + backend forward | high – decision computed server-side | production, payments, anything money-shaped |
| C · Hybrid token | 1 line of HTML + 1 backend call | high – browser-collected, backend-verified | best 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
<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
<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.
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
<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
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
<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
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)
{
"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
- Never embed your server API key in browser JS. Pattern A uses the tenant slug (a public identifier) and is per-IP rate-limited.
- Always pass the real client IP from your backend (Pattern B) — most reverse proxies set X-Forwarded-For.
- Don't reveal to end-users which rule triggered. A generic “please verify your email” is enough; fraudsters tune around exposed reasons.
- Rotate API keys periodically; revocation is instant in /client-admin/api-keys.
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.