Documentation
Docs API Reference SDKs Guides Changelog Status
v4.1
Log in Dashboard →

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.

📋
What's detected: Selenium, Puppeteer, Playwright, PhantomJS, headless Chrome/Firefox, modified navigator properties, CDP protocol artifacts, WebDriver flags, automation-injected JavaScript, and dozens of other signals.

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.

JavaScript
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.

Node.js — server.js
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' });
});
Python — server.py
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 conditionActionDescription
signals.bot.detected == trueBlockBlock all detected bots immediately. Catches headless browsers, automation tools, and known bot frameworks.
signals.bot.type == "crawler"ChallengeChallenge crawlers with a verification step. Useful if you want to allow legitimate search engines but block scrapers.
scores.bot > 0.8BlockBlock visitors with high bot ML scores even if no explicit bot signal fires. Catches sophisticated bots.
signals.tampered == trueChallengeChallenge 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:

1Start with challenge instead of block for new rules to observe impact before enforcing.
2Use the Rule Backtesting feature to simulate a rule against historical traffic before deploying.
3Allowlist known bots (e.g., Googlebot, monitoring services) by IP or user agent via Lists.
4Review the signals.bot.type field to understand what category of bot is being detected.
5Set up webhooks to get real-time Slack alerts when bot rules fire.
💡
Tip: Combine bot detection with velocity metrics. A visitor making 50 requests per minute is likely automated even if no explicit bot signal fires.

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.

⚠️
Why passwords alone fail: Over 15 billion credentials are available in breach databases. Attackers use automated tools to test stolen credentials at scale. Device fingerprinting adds a layer that stolen credentials cannot replicate.

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.

SignalTypeATO relevance
visitorFoundbooleanIf false, this is the first time this device has been seen. High risk for ATO if the account is established.
scores.atofloatML-based ATO risk score from 0 to 1. Incorporates device history, velocity, signal anomalies, and behavioral patterns.
signals.vpnobjectVPN/proxy detected. Combined with a new device, strongly suggests credential stuffing or unauthorized access attempt.
signals.incognitobooleanPrivate browsing mode. Legitimate users sometimes use incognito, but combined with other signals it increases risk.
signals.impossibleTravelobjectLogin 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.

Node.js — login-handler.js
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) });
});
Python — login_handler.py
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 conditionActionDescription
scores.ato > 0.7BlockBlock logins with very high ATO risk. Catches credential stuffing and stolen sessions.
scores.ato > 0.4 AND signals.vpnChallengeRequire MFA for medium-risk logins from VPN or proxy connections.
signals.impossibleTravelBlockBlock if the user's location is impossible given their last login (e.g., NYC to Tokyo in 30 minutes).
velocity.failed_logins_1h > 5BlockBlock after 5 failed login attempts from the same device within an hour.
💡
Best practice: Store the 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:

SignalTypeFraud relevance
scores.abusefloatGeneral abuse score (0-1). Aggregates device risk, velocity, signal anomalies, and behavioral fingerprint.
scores.suspectintegerSuspicion score from 0-100. Higher values correlate with higher chargeback rates. Threshold of 60+ catches 90% of fraud.
signals.vpnobjectVPN/proxy usage. Fraudsters use VPNs to mask their true location and bypass geo-restrictions.
signals.emulatorbooleanDevice emulation detected. Indicates a virtual device used for bulk fraud operations.
velocity.payments_24hintegerNumber 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.

Node.js — payment-check.js
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.

Node.js — checkout.js
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.

1Store the visitorId and requestId in your payment metadata for every transaction.
2When a chargeback is filed, retrieve the full Prynt event to include device intelligence as evidence.
3Add the visitorId to a blocklist via the Lists API to prevent future transactions from that device.
4Feed chargeback outcomes back into your risk thresholds -- lower the block threshold if chargebacks increase.
5Use backtesting to simulate stricter rules against past chargebacks to find the optimal threshold.
Node.js — chargeback-handler.js
// 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:

+Ad blocker bypass: Requests to metrics.yourdomain.com are not blocked by filter lists.
+Higher identification rates: Typically 8-12% more identifications on sites with tech-savvy audiences.
+First-party cookies: Cookies are set on your domain, extending cookie lifetime from 7 days to 400+ days.
+Network resilience: If Prynt's edge CDN experiences issues, your DNS can failover to a backup.
📋
Recommended subdomain: Use something generic like 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.

RegionCNAME targetNotes
US (default)proxy-us.prynt.ioUS East data center. Best for North/South American traffic.
EUproxy-eu.prynt.ioFrankfurt data center. Required for GDPR-compliant data residency.
APproxy-ap.prynt.ioSingapore data center. Best for Asia-Pacific traffic.
DNS Record
# 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.

nginx.conf
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.

Cloudflare Worker — proxy.js
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.

JavaScript — app.js
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
💡
Verification checklist: Open DevTools > Network tab and confirm that Prynt requests go to 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.

FeatureStandard flowSealed results flow
Client responseUnsigned visitorId + requestIdEncrypted blob with full data
Server verificationAPI call to GET /v1/eventsLocal decryption (no API call)
Latency~50ms for client + ~30ms for server~50ms total (client only)
Tamper-proofOnly after server verificationAlways (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.

JavaScript — client.js
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 }),
});
Node.js — server.js
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 });
});
Python — server.py
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:

+Tamper-proof by default: The encrypted payload cannot be modified in transit. Any tampering invalidates the decryption.
+No network dependency: Server-side verification happens locally via decryption, so there is no dependency on Prynt's API being reachable from your server.
+Reduced attack surface: No Server API key is transmitted over the network for verification calls. The secret key is only used for local decryption.
+Lower latency: Eliminates the ~30ms server-to-server API call. Total round-trip is just the client identification time.
⚠️
Important: Even with sealed results, never trust the client-side SDK response for security decisions. Always send the 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:

?How many visitors would this rule have blocked last week?
?What percentage of my traffic would be challenged?
?Would this rule have caught the fraud I saw on Tuesday?
?Am I setting my thresholds too aggressively or too conservatively?
📋
Availability: Rule backtesting is available on Pro and Enterprise plans. Backtests can analyze up to 30 days of historical data and up to 1 million events per run.

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.

1Go to Dashboard > Rules and click New Rule or select an existing rule.
2Configure the rule conditions (e.g., scores.ato > 0.5 AND signals.vpn.detected == true).
3Set the action (block, challenge, or flag).
4Click Backtest and select the time range (1 day, 7 days, or 30 days).
5Review the results: total events matched, breakdown by action, and sample matched events.

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.

POST/v1/rules/{ruleId}/backtest
Node.js — backtest.js
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);
Python — backtest.py
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.

FieldTypeDescription
totalEventsintegerTotal identification events in the selected time range.
matchedEventsintegerNumber of events that would have triggered the rule.
matchRatefloatPercentage of total events matched. Aim for <5% for block rules to minimize false positives.
uniqueVisitorsintegerNumber of unique visitorId values matched. Helps distinguish bots (1 visitor, many events) from broad rules.
samplesarrayUp to 100 sample matched events for manual review. Includes full signals and scores.
timelinearrayHourly breakdown of matched events. Useful for identifying if matches cluster at specific times.

Best practices

💡
Start conservative, iterate: Begin with strict thresholds (e.g., 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.
1Test one condition at a time. Add conditions incrementally and backtest after each addition to understand the impact of each filter.
2Review sample matches manually. Check the samples array to verify that matched events are actually suspicious, not legitimate users.
3Use challenge before block. Deploy new rules with challenge action first. After a week of monitoring, upgrade to block if false positives are acceptable.
4Backtest against known fraud. If you have records of confirmed fraud, check whether the backtest catches those specific events.
5Re-backtest periodically. Traffic patterns change. A rule tuned for last month's traffic may need adjustment. Re-backtest monthly.

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.

📋
Stability: Prynt's fingerprint remains stable across browser updates, minor OS updates, and cookie clearing. The 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.

SignalDescription
botDetects automated browsers, headless browsers, Selenium, Puppeteer, and other automation frameworks.
vpnIdentifies VPN, proxy, and anonymizing network connections with provider and type details.
incognitoDetects private/incognito browsing mode across all major browsers.
tamperedIdentifies when browser properties have been modified to spoof device identity.
emulatorDetects virtual machines and device emulators used for bulk fraud operations.
torIdentifies connections through the Tor network.
impossibleTravelDetects logins from locations that are geographically impossible given previous activity.
jailbrokenDetects 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 metricDescription
ids_per_device_24hNumber of identification calls from the same device in 24 hours. High values suggest automation.
accounts_per_device_7dUnique accounts associated with this device in 7 days. High values indicate multi-accounting.
devices_per_ip_1hUnique devices from the same IP in 1 hour. High values suggest credential stuffing from a single origin.
failed_logins_1hFailed 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.

ScoreRangeWhat it measures
scores.abuse0 - 1.0General abuse likelihood. Combines device risk, velocity anomalies, and behavioral signals.
scores.ato0 - 1.0Account takeover risk. Weighs device trust, login patterns, impossible travel, and credential-stuffing signals.
scores.bot0 - 1.0Bot probability. Uses browser fingerprint consistency, API behavior, and automation artifact detection.
scores.suspect0 - 100Composite suspicion score. Higher values correlate with higher fraud rates across all categories.
💡
Recommended thresholds: Start with 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.

+No PII collection: The SDK collects only device attributes (canvas hash, WebGL parameters, etc.), not user data.
+GDPR compliant: Device fingerprinting for fraud prevention is covered under Legitimate Interest (Art. 6(1)(f) GDPR).
+SOC 2 Type II: Prynt maintains SOC 2 Type II certification with annual third-party audits.
+Data deletion: Use the DELETE /v1/visitors/{visitorId} endpoint to delete all data associated with a visitor.
+Retention controls: Configure data retention periods (30, 90, 180, or 365 days) in dashboard settings.

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.

RegionLocationBest for
USVirginia, USADefault region. Best latency for North and South American traffic. No data residency restrictions.
EUFrankfurt, GermanyRequired for GDPR compliance when data must not leave the EU. Covers EU/EEA and UK traffic.
APSingaporeBest latency for Asia-Pacific traffic. Suitable for PDPA (Singapore) and APPI (Japan) compliance.
📋
Setting your region: Region is configured at the SDK level via the 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.