Implementation Guides
Step-by-step guides for implementing Prynt's fraud prevention, device intelligence, and security features in your application. Each guide includes working code examples in JavaScript and Python.
Bot Protection Guide
Overview of bot threats
Automated bots account for nearly 40% of all internet traffic, and a significant portion of that is malicious. Bots can scrape your content, stuff credentials, create fake accounts, hoard inventory, and abuse promotions. Traditional CAPTCHAs frustrate real users and sophisticated bots bypass them easily.
Prynt's bot detection uses 80+ device signals to distinguish real humans from automated agents, headless browsers, browser automation tools, and emulators -- without any user friction. Detection happens passively during the identify() call and results are available both client-side and via the Server API.
Setting up bot detection
Bot detection is enabled by default on every Prynt plan. The signals.bot object in every identification response tells you whether a bot was detected and what type it is. Here is how to integrate it.
Step 1: Client-side identification
Add the Prynt SDK to your frontend and call identify() on the page or action you want to protect. Send the requestId to your server for verification.
import { Prynt } from '@prynt/sdk'; const prynt = new Prynt({ apiKey: 'pk_live_xxxxxxxxxxxx', }); // Identify on page load or before a protected action const { requestId } = await prynt.identify(); // Send to your server for verification const response = await fetch('/api/check-bot', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requestId }), });
Step 2: Server-side verification and bot check
On your server, use the Server API to retrieve the full identification result and check the bot signals. Never trust client-side results for security decisions.
import { PryntServer } from '@prynt/node'; const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY, }); app.post('/api/check-bot', async (req, res) => { const event = await prynt.getEvent(req.body.requestId); if (event.signals.bot.detected) { console.log('Bot detected:', event.signals.bot.type); return res.status(403).json({ error: 'bot_detected', botType: event.signals.bot.type, }); } // Not a bot -- proceed res.json({ action: 'allow' }); });
from prynt import PryntServer from flask import Flask, request, jsonify app = Flask(__name__) prynt = PryntServer(secret_key=os.environ["PRYNT_SECRET_KEY"]) @app.route("/api/check-bot", methods=["POST"]) def check_bot(): request_id = request.json["requestId"] event = prynt.get_event(request_id) if event.signals.bot.detected: return jsonify({ "error": "bot_detected", "botType": event.signals.bot.type, }), 403 return jsonify({"action": "allow"})
Configuring rules for bot blocking
Instead of writing code for every bot check, you can configure rules in the Prynt dashboard that automatically return verdicts. Rules are evaluated server-side on every identification and the result is returned in the verdict field.
| Rule condition | Action | Description |
|---|---|---|
| signals.bot.detected == true | Block | Block all detected bots immediately. Catches headless browsers, automation tools, and known bot frameworks. |
| signals.bot.type == "crawler" | Challenge | Challenge crawlers with a verification step. Useful if you want to allow legitimate search engines but block scrapers. |
| scores.bot > 0.8 | Block | Block visitors with high bot ML scores even if no explicit bot signal fires. Catches sophisticated bots. |
| signals.tampered == true | Challenge | Challenge requests from browsers with tampered properties. May indicate a cloaked automation tool. |
Monitoring and tuning false positives
After enabling bot rules, monitor the Rules activity panel in the dashboard to review blocked and challenged requests. Common tuning strategies include:
challenge instead of block for new rules to observe impact before enforcing.signals.bot.type field to understand what category of bot is being detected.Account Takeover Prevention Guide
Understanding ATO attacks
Account takeover (ATO) occurs when an attacker gains unauthorized access to a user's account, typically through credential stuffing, phishing, or session hijacking. ATO is one of the fastest-growing fraud vectors, costing businesses billions annually.
Prynt helps prevent ATO by linking device identity to user accounts. When a login attempt comes from an unrecognized device, elevated risk scores, or suspicious signals (VPN, tampered browser, impossible travel), you can require additional verification before granting access.
Detecting suspicious login patterns
Prynt provides several signals that are especially relevant for ATO detection. Combine these signals to build a risk profile for each login attempt.
| Signal | Type | ATO relevance |
|---|---|---|
| visitorFound | boolean | If false, this is the first time this device has been seen. High risk for ATO if the account is established. |
| scores.ato | float | ML-based ATO risk score from 0 to 1. Incorporates device history, velocity, signal anomalies, and behavioral patterns. |
| signals.vpn | object | VPN/proxy detected. Combined with a new device, strongly suggests credential stuffing or unauthorized access attempt. |
| signals.incognito | boolean | Private browsing mode. Legitimate users sometimes use incognito, but combined with other signals it increases risk. |
| signals.impossibleTravel | object | Login from a location that is geographically impossible given the last login time and distance. |
Implementing device trust scoring
Device trust is the practice of tracking which devices a user has historically logged in from and flagging new or suspicious devices. Prynt's persistent visitorId makes this straightforward.
import { PryntServer } from '@prynt/node'; import { db } from './database'; const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY, }); app.post('/api/login', async (req, res) => { const { email, password, requestId } = req.body; // 1. Validate credentials (your existing auth logic) const user = await db.verifyCredentials(email, password); if (!user) return res.status(401).json({ error: 'invalid_credentials' }); // 2. Get Prynt identification const event = await prynt.getEvent(requestId); const { visitorId, signals, scores } = event; // 3. Check if this device is trusted for this user const trustedDevices = await db.getTrustedDevices(user.id); const isTrusted = trustedDevices.includes(visitorId); // 4. Evaluate risk if (scores.ato > 0.7 || signals.impossibleTravel?.detected) { // High risk -- block and notify account owner await notifyUser(user, 'suspicious_login'); return res.status(403).json({ error: 'login_blocked', reason: 'suspicious_activity', }); } if (!isTrusted || signals.vpn?.detected) { // Medium risk -- require MFA return res.json({ action: 'require_mfa', visitorId: visitorId, }); } // 5. Low risk -- allow login, update last seen await db.updateLastSeen(user.id, visitorId); res.json({ action: 'allow', token: generateToken(user) }); });
from prynt import PryntServer from flask import Flask, request, jsonify prynt = PryntServer(secret_key=os.environ["PRYNT_SECRET_KEY"]) @app.route("/api/login", methods=["POST"]) def login(): data = request.json user = verify_credentials(data["email"], data["password"]) if not user: return jsonify({"error": "invalid_credentials"}), 401 event = prynt.get_event(data["requestId"]) trusted = get_trusted_devices(user.id) # High risk: block if event.scores.ato > 0.7: return jsonify({"error": "login_blocked"}), 403 # Unknown device: require MFA if event.visitor_id not in trusted: return jsonify({"action": "require_mfa"}), 200 # Trusted device: allow return jsonify({"action": "allow", "token": generate_token(user)})
Setting up alerts and rules
Configure dashboard rules to automatically flag or block high-risk logins. Set up webhooks to receive real-time alerts when ATO conditions are triggered.
| Rule condition | Action | Description |
|---|---|---|
| scores.ato > 0.7 | Block | Block logins with very high ATO risk. Catches credential stuffing and stolen sessions. |
| scores.ato > 0.4 AND signals.vpn | Challenge | Require MFA for medium-risk logins from VPN or proxy connections. |
| signals.impossibleTravel | Block | Block if the user's location is impossible given their last login (e.g., NYC to Tokyo in 30 minutes). |
| velocity.failed_logins_1h > 5 | Block | Block after 5 failed login attempts from the same device within an hour. |
visitorId alongside each user session. When a user confirms a device via MFA, add its visitorId to the trusted devices list so they are not challenged again from that device.Payment Fraud Prevention Guide
Payment fraud signals
Payment fraud costs online businesses an estimated $48 billion annually. Common attack vectors include stolen credit cards, account takeover for saved payment methods, and promo abuse across multiple fake accounts. Prynt helps you detect and prevent fraud before a charge is processed.
Key signals for payment fraud detection:
| Signal | Type | Fraud relevance |
|---|---|---|
| scores.abuse | float | General abuse score (0-1). Aggregates device risk, velocity, signal anomalies, and behavioral fingerprint. |
| scores.suspect | integer | Suspicion score from 0-100. Higher values correlate with higher chargeback rates. Threshold of 60+ catches 90% of fraud. |
| signals.vpn | object | VPN/proxy usage. Fraudsters use VPNs to mask their true location and bypass geo-restrictions. |
| signals.emulator | boolean | Device emulation detected. Indicates a virtual device used for bulk fraud operations. |
| velocity.payments_24h | integer | Number of payment attempts from this device in 24 hours. Spike indicates card testing or promo abuse. |
Risk scoring for transactions
Build a composite risk score by combining Prynt's ML scores with your own business logic. The recommended approach is to create risk tiers that map to different actions.
import { PryntServer } from '@prynt/node'; const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY, }); async function evaluatePaymentRisk(requestId, amount) { const event = await prynt.getEvent(requestId); const { scores, signals, visitorId } = event; // Composite risk score let risk = scores.abuse * 100; // Boost risk for suspicious signals if (signals.vpn?.detected) risk += 15; if (signals.tor) risk += 30; if (signals.emulator) risk += 25; if (signals.incognito) risk += 10; if (signals.tampered) risk += 20; // Amount-based risk adjustment if (amount > 500) risk += 10; if (amount > 2000) risk += 20; // Decision if (risk >= 70) return { action: 'block', risk }; if (risk >= 40) return { action: 'review', risk }; if (risk >= 20) return { action: 'challenge', risk }; return { action: 'allow', risk }; }
Implementing pre-authorization checks
Run the Prynt check before authorizing the payment with your payment processor. This prevents chargebacks and saves processing fees on fraudulent transactions.
app.post('/api/checkout', async (req, res) => { const { requestId, amount, paymentMethodId } = req.body; // Step 1: Evaluate risk BEFORE charging const riskResult = await evaluatePaymentRisk(requestId, amount); if (riskResult.action === 'block') { return res.status(403).json({ error: 'payment_declined', message: 'This transaction could not be processed.', }); } if (riskResult.action === 'challenge') { return res.json({ action: 'verify_identity', message: 'Additional verification required.', }); } // Step 2: Process payment (low risk) const charge = await stripe.paymentIntents.create({ amount: amount, payment_method: paymentMethodId, confirm: true, metadata: { prynt_visitor_id: riskResult.visitorId, prynt_risk_score: riskResult.risk.toString(), }, }); res.json({ action: 'charged', chargeId: charge.id }); });
Chargeback prevention workflow
Even with pre-authorization checks, some fraud slips through. Use Prynt data to strengthen chargeback disputes and refine your detection over time.
visitorId and requestId in your payment metadata for every transaction.visitorId to a blocklist via the Lists API to prevent future transactions from that device.// When a chargeback is received, blocklist the device app.post('/webhooks/chargeback', async (req, res) => { const { chargeId } = req.body; // Get the visitorId from your payment record const payment = await db.getPayment(chargeId); const visitorId = payment.metadata.prynt_visitor_id; // Add to blocklist via Prynt Lists API await prynt.lists.addEntries('lst_fraud_devices', [ { type: 'visitor_id', value: visitorId }, ]); console.log(`Blocklisted device ${visitorId} for chargeback`); res.sendStatus(200); });
Subdomain Proxy Setup Guide
Why use a proxy
By default, the Prynt SDK communicates directly with api.prynt.io. While this works for most users, ad blockers and privacy extensions may block requests to known third-party domains. A subdomain proxy routes Prynt traffic through your own domain, making it indistinguishable from first-party requests.
Benefits of a subdomain proxy:
metrics.yourdomain.com are not blocked by filter lists.metrics.yourdomain.com or edge.yourdomain.com. Avoid names that reference fingerprinting or tracking, as advanced filter lists may flag them.DNS configuration steps
Create a CNAME record pointing your chosen subdomain to Prynt's proxy endpoint. The exact target depends on your data residency region.
| Region | CNAME target | Notes |
|---|---|---|
| US (default) | proxy-us.prynt.io | US East data center. Best for North/South American traffic. |
| EU | proxy-eu.prynt.io | Frankfurt data center. Required for GDPR-compliant data residency. |
| AP | proxy-ap.prynt.io | Singapore data center. Best for Asia-Pacific traffic. |
# Add this CNAME record in your DNS provider Type: CNAME Name: metrics # becomes metrics.yourdomain.com Value: proxy-us.prynt.io TTL: 300 # 5 minutes
Nginx reverse proxy setup
If you prefer to use your own reverse proxy instead of a CNAME, here is an Nginx configuration that proxies requests to Prynt.
server { listen 443 ssl; server_name metrics.yourdomain.com; ssl_certificate /etc/ssl/certs/metrics.crt; ssl_certificate_key /etc/ssl/private/metrics.key; location / { proxy_pass https://api.prynt.io; proxy_set_header Host api.prynt.io; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Real-IP $remote_addr; proxy_ssl_server_name on; # Cache control -- do not cache identification responses proxy_cache off; proxy_buffering off; } }
Cloudflare Workers setup
If you use Cloudflare, a Worker provides a zero-infrastructure proxy that runs at the edge, close to your users.
export default { async fetch(request) { const url = new URL(request.url); // Rewrite to Prynt API url.hostname = 'api.prynt.io'; const proxyRequest = new Request(url, { method: request.method, headers: request.headers, body: request.body, }); // Forward client IP for accurate geolocation proxyRequest.headers.set( 'X-Forwarded-For', request.headers.get('CF-Connecting-IP') ); return fetch(proxyRequest); }, }
Verifying proxy is working
After setting up the proxy, update your SDK configuration to use the custom endpoint and verify that identification still works correctly.
import { Prynt } from '@prynt/sdk'; const prynt = new Prynt({ apiKey: 'pk_live_xxxxxxxxxxxx', endpoint: 'https://metrics.yourdomain.com', // your proxy }); // Test identification through the proxy const result = await prynt.identify(); console.log('Visitor ID:', result.visitorId); console.log('Proxied:', result.meta.endpoint); // should show your domain
metrics.yourdomain.com instead of api.prynt.io. The response should contain a valid visitorId. Also test with an ad blocker enabled to confirm requests are not blocked.Sealed Results Guide
What are sealed results
Sealed results are encrypted, tamper-proof identification payloads that are returned directly from the client SDK. Unlike standard results (which require a separate Server API call to verify), sealed results contain the full identification data encrypted with your account's public key. Only your server, with the corresponding private key, can decrypt them.
This eliminates the need for a server-to-server API call for every identification, reducing latency and simplifying your architecture.
| Feature | Standard flow | Sealed results flow |
|---|---|---|
| Client response | Unsigned visitorId + requestId | Encrypted blob with full data |
| Server verification | API call to GET /v1/events | Local decryption (no API call) |
| Latency | ~50ms for client + ~30ms for server | ~50ms total (client only) |
| Tamper-proof | Only after server verification | Always (encrypted at source) |
Server-side decryption
When sealed results are enabled, the identify() call returns a sealedResult field containing the encrypted payload. Send this to your server and decrypt it using your secret key.
import { Prynt } from '@prynt/sdk'; const prynt = new Prynt({ apiKey: 'pk_live_xxxxxxxxxxxx', sealed: true, // Enable sealed results }); const { sealedResult } = await prynt.identify(); // Send the encrypted blob to your server await fetch('/api/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sealedResult }), });
import { PryntServer } from '@prynt/node'; const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY, }); app.post('/api/verify', async (req, res) => { // Decrypt the sealed result locally -- no API call needed const event = await prynt.unsealResult(req.body.sealedResult); // Full identification data is now available console.log(event.visitorId); // "pv_8kX2mNqR3jT7p" console.log(event.confidence); // 0.995 console.log(event.signals.bot); // { detected: false } console.log(event.scores); // { abuse: 0.03, ato: 0.01, ... } // Make decisions as usual if (event.signals.bot.detected) { return res.status(403).json({ error: 'bot_detected' }); } res.json({ action: 'allow', visitorId: event.visitorId }); });
from prynt import PryntServer prynt = PryntServer(secret_key=os.environ["PRYNT_SECRET_KEY"]) @app.route("/api/verify", methods=["POST"]) def verify(): sealed = request.json["sealedResult"] event = prynt.unseal_result(sealed) # Full data available without API call if event.signals.bot.detected: return jsonify({"error": "bot_detected"}), 403 return jsonify({"action": "allow"})
Security benefits
Sealed results provide several security advantages over the standard verification flow:
sealedResult to your server and decrypt it there. The client-side response is only a convenience for non-security use cases like analytics.Rule Backtesting Guide New
What is backtesting
Backtesting lets you simulate a rule against your historical traffic before deploying it to production. Instead of deploying a rule and hoping it does not generate false positives, you can see exactly how it would have performed against real identification events from the past 30 days.
Backtesting answers questions like:
Running a backtest via the dashboard
The easiest way to backtest is through the Prynt dashboard. Navigate to Rules, create or select a rule, and click Backtest.
scores.ato > 0.5 AND signals.vpn.detected == true).block, challenge, or flag).Running a backtest via API
For automated testing or CI/CD integration, use the Backtesting API endpoint. This is useful for testing multiple rule variations programmatically.
import { PryntServer } from '@prynt/node'; const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY, }); // Create a rule and backtest it const rule = await prynt.rules.create({ name: 'High-risk ATO block', conditions: [ { field: 'scores.ato', op: 'gt', value: 0.7 }, { field: 'signals.vpn.detected', op: 'eq', value: true }, ], action: 'block', enabled: false, // Don't enable yet }); // Run backtest against last 7 days const backtest = await prynt.rules.backtest(rule.id, { days: 7, }); console.log('Total events analyzed:', backtest.totalEvents); console.log('Events matched:', backtest.matchedEvents); console.log('Match rate:', backtest.matchRate); console.log('Sample matches:', backtest.samples);
from prynt import PryntServer prynt = PryntServer(secret_key=os.environ["PRYNT_SECRET_KEY"]) # Create rule (disabled) and backtest rule = prynt.rules.create( name="High-risk ATO block", conditions=[ {"field": "scores.ato", "op": "gt", "value": 0.7}, {"field": "signals.vpn.detected", "op": "eq", "value": True}, ], action="block", enabled=False, ) backtest = prynt.rules.backtest(rule.id, days=7) print(f"Analyzed: {backtest.total_events} events") print(f"Matched: {backtest.matched_events} events") print(f"Rate: {backtest.match_rate}")
Interpreting results
The backtest response contains detailed metrics to help you evaluate the rule before enabling it.
| Field | Type | Description |
|---|---|---|
| totalEvents | integer | Total identification events in the selected time range. |
| matchedEvents | integer | Number of events that would have triggered the rule. |
| matchRate | float | Percentage of total events matched. Aim for <5% for block rules to minimize false positives. |
| uniqueVisitors | integer | Number of unique visitorId values matched. Helps distinguish bots (1 visitor, many events) from broad rules. |
| samples | array | Up to 100 sample matched events for manual review. Includes full signals and scores. |
| timeline | array | Hourly breakdown of matched events. Useful for identifying if matches cluster at specific times. |
Best practices
scores.ato > 0.9) and gradually lower them while monitoring the match rate. A rule matching more than 5-10% of traffic likely needs refinement.samples array to verify that matched events are actually suspicious, not legitimate users.challenge before block. Deploy new rules with challenge action first. After a week of monitoring, upgrade to block if false positives are acceptable.Concept: Device Fingerprinting
Device fingerprinting is the process of collecting and analyzing hardware, software, and configuration attributes of a device to generate a unique, persistent identifier. Unlike cookies, a fingerprint survives cookie clearing, incognito mode, and browser reinstallation.
Prynt collects 80+ signals including canvas rendering, WebGL parameters, audio processing characteristics, installed fonts, screen properties, GPU identification, timezone, language settings, and dozens of browser API behaviors. These are combined using a proprietary hashing algorithm to produce a stable visitorId.
confidence score (0-1) indicates how certain the identification is. Values above 0.9 indicate very high certainty.Concept: Smart Signals
Smart Signals are real-time intelligence signals derived from device analysis during identification. Each signal detects a specific attribute or behavior that may indicate fraud, automation, or privacy-tool usage.
| Signal | Description |
|---|---|
| bot | Detects automated browsers, headless browsers, Selenium, Puppeteer, and other automation frameworks. |
| vpn | Identifies VPN, proxy, and anonymizing network connections with provider and type details. |
| incognito | Detects private/incognito browsing mode across all major browsers. |
| tampered | Identifies when browser properties have been modified to spoof device identity. |
| emulator | Detects virtual machines and device emulators used for bulk fraud operations. |
| tor | Identifies connections through the Tor network. |
| impossibleTravel | Detects logins from locations that are geographically impossible given previous activity. |
| jailbroken | Detects jailbroken iOS or rooted Android devices (mobile SDK only). |
Concept: Velocity Metrics
Velocity metrics track the rate of actions performed by a device, IP address, or account over time. They are essential for detecting abuse patterns that individual signals cannot catch, such as rapid account creation, payment card testing, or credential stuffing.
Prynt provides built-in velocity counters and lets you define custom ones. Velocity data is available in the velocity object of every identification response and can be used in rule conditions.
| Built-in metric | Description |
|---|---|
| ids_per_device_24h | Number of identification calls from the same device in 24 hours. High values suggest automation. |
| accounts_per_device_7d | Unique accounts associated with this device in 7 days. High values indicate multi-accounting. |
| devices_per_ip_1h | Unique devices from the same IP in 1 hour. High values suggest credential stuffing from a single origin. |
| failed_logins_1h | Failed login attempts from this device per hour. Configure via tagged events. |
Concept: ML Scoring
Prynt's ML models analyze hundreds of features from device signals, behavioral patterns, and historical data to produce risk scores for every identification event. Scores are available on Pro and Enterprise plans.
| Score | Range | What it measures |
|---|---|---|
| scores.abuse | 0 - 1.0 | General abuse likelihood. Combines device risk, velocity anomalies, and behavioral signals. |
| scores.ato | 0 - 1.0 | Account takeover risk. Weighs device trust, login patterns, impossible travel, and credential-stuffing signals. |
| scores.bot | 0 - 1.0 | Bot probability. Uses browser fingerprint consistency, API behavior, and automation artifact detection. |
| scores.suspect | 0 - 100 | Composite suspicion score. Higher values correlate with higher fraud rates across all categories. |
scores.abuse > 0.7 for blocking and > 0.4 for challenging. These thresholds catch 85%+ of fraud with <1% false positive rate. Tune based on your backtesting results.Concept: Data Privacy
Prynt is designed with privacy-by-design principles. Device fingerprinting collects only technical device attributes (hardware, software, configuration) and does not collect any personally identifiable information (PII) such as names, email addresses, or browsing history.
DELETE /v1/visitors/{visitorId} endpoint to delete all data associated with a visitor.Concept: Data Residency
Prynt offers three data residency regions to meet regulatory and compliance requirements. Data is processed and stored exclusively within the selected region.
| Region | Location | Best for |
|---|---|---|
| US | Virginia, USA | Default region. Best latency for North and South American traffic. No data residency restrictions. |
| EU | Frankfurt, Germany | Required for GDPR compliance when data must not leave the EU. Covers EU/EEA and UK traffic. |
| AP | Singapore | Best latency for Asia-Pacific traffic. Suitable for PDPA (Singapore) and APPI (Japan) compliance. |
region parameter. Once set, all identification data is processed and stored in that region. You can use different regions for different applications within the same account.