Back to Blog

Product Authentication with QR Codes: Anti-Counterfeiting API

QRWorks Team11 min read

Counterfeits cost brands $500 billion annually. Your customers want to verify they bought the real thing.

A QR code on each product solves this. Customers scan to confirm authenticity. You see where products are being scanned—and catch gray market distributors.

In this tutorial, you'll build a product authentication system that:

  • Generates unique verification codes for each product unit
  • Validates authenticity with a customer-facing scan
  • Detects suspicious patterns (multiple scans, wrong geography)
  • Tracks distribution channels via scan location data

How it works

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  Product    │────▶│  QRWorks    │────▶│  Your App   │
│  Package    │     │  Redirect   │     │  Verify     │
└─────────────┘     └─────────────┘     └─────────────┘
       │                                       │
       │                                       ▼
       │                              ┌─────────────────┐
       │                              │  ✓ Authentic    │
       │                              │  First scan     │
       │                              │  Expected region│
       │                              └─────────────────┘
       │                                       │
       ▼                                       ▼
┌─────────────┐                       ┌─────────────────┐
│  Analytics  │◀──────────────────────│  ⚠ Warning     │
│  Dashboard  │                       │  Multiple scans│
│  (detect    │                       │  Wrong region  │
│   fraud)    │                       └─────────────────┘
└─────────────┘

A genuine product scans once, from the expected market. Counterfeits show patterns: multiple scans of the same code, scans from unexpected countries.

Database schema

-- products table
CREATE TABLE products (
  id UUID PRIMARY KEY,
  sku VARCHAR(100) NOT NULL,
  name VARCHAR(255) NOT NULL,
  batch_number VARCHAR(100),
  manufactured_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

-- product_units table (individual serialized units)
CREATE TABLE product_units (
  id UUID PRIMARY KEY,
  product_id UUID REFERENCES products(id),
  serial_number VARCHAR(100) UNIQUE NOT NULL,
  qr_analytics_id VARCHAR(100),
  intended_market VARCHAR(100), -- US, EU, APAC, etc.
  distributor_id UUID,
  status VARCHAR(50) DEFAULT 'unverified',
  first_verified_at TIMESTAMP,
  verification_count INTEGER DEFAULT 0,
  created_at TIMESTAMP DEFAULT NOW()
);

-- verification_events table
CREATE TABLE verification_events (
  id UUID PRIMARY KEY,
  unit_id UUID REFERENCES product_units(id),
  scanned_at TIMESTAMP DEFAULT NOW(),
  ip_address VARCHAR(50),
  country VARCHAR(100),
  city VARCHAR(100),
  device_type VARCHAR(100),
  result VARCHAR(50), -- authentic, suspect, counterfeit
  flags TEXT[]
);

Generate product authentication codes

When manufacturing or packaging products, generate unique QR codes:

// products.js
import fetch from 'node-fetch';
import crypto from 'crypto';

const API_KEY = process.env.QRWORKS_API_KEY;
const BASE_URL = process.env.QRWORKS_BASE_URL;

async function createProductUnit(product, serialNumber, intendedMarket) {
  // Generate verification URL with cryptographic signature
  const signature = crypto
    .createHmac('sha256', process.env.SIGNING_SECRET)
    .update(serialNumber)
    .digest('hex')
    .slice(0, 12);

  const verifyUrl = `https://verify.yourbrand.com/check/${serialNumber}/${signature}`;

  const response = await fetch(`${BASE_URL}/v1/generate/dynamic`, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      redirect_url: verifyUrl,
      metadata: {
        product_id: product.id,
        serial_number: serialNumber,
        sku: product.sku,
        intended_market: intendedMarket,
        type: 'product_auth'
      }
    })
  });

  const data = await response.json();

  // Save to database
  await db.query(`
    INSERT INTO product_units
    (id, product_id, serial_number, qr_analytics_id, intended_market)
    VALUES ($1, $2, $3, $4, $5)
  `, [generateId(), product.id, serialNumber, data.analytics_id, intendedMarket]);

  return {
    serialNumber,
    qrCodeUrl: data.qr_code_url,
    analyticsId: data.analytics_id
  };
}

The signature prevents attackers from guessing valid URLs. Only codes generated by your system will verify.

Verification endpoint

When a customer scans the product:

// server.js
import express from 'express';

const app = express();

app.get('/check/:serial/:signature', async (req, res) => {
  const { serial, signature } = req.params;

  // Validate signature
  const expectedSignature = crypto
    .createHmac('sha256', process.env.SIGNING_SECRET)
    .update(serial)
    .digest('hex')
    .slice(0, 12);

  if (signature !== expectedSignature) {
    return res.status(400).json({
      result: 'invalid',
      message: 'Invalid verification code'
    });
  }

  // Get product unit
  const unit = await db.query(
    'SELECT * FROM product_units WHERE serial_number = $1',
    [serial]
  ).then(r => r.rows[0]);

  if (!unit) {
    return res.status(404).json({
      result: 'not_found',
      message: 'Product not found in our database'
    });
  }

  // Get scan context
  const scanContext = {
    ip: req.headers['x-forwarded-for'] || req.ip,
    country: req.headers['cf-ipcountry'] || await geolocate(req.ip),
    city: await getCityFromIP(req.ip),
    device: parseUserAgent(req.headers['user-agent'])
  };

  // Analyze for fraud indicators
  const analysis = await analyzeVerification(unit, scanContext);

  // Log verification event
  await db.query(`
    INSERT INTO verification_events
    (id, unit_id, ip_address, country, city, device_type, result, flags)
    VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
  `, [
    generateId(),
    unit.id,
    scanContext.ip,
    scanContext.country,
    scanContext.city,
    scanContext.device,
    analysis.result,
    analysis.flags
  ]);

  // Update unit stats
  await db.query(`
    UPDATE product_units
    SET verification_count = verification_count + 1,
        first_verified_at = COALESCE(first_verified_at, NOW()),
        status = $1
    WHERE id = $2
  `, [analysis.result, unit.id]);

  res.json(analysis);
});

Fraud detection logic

Analyze scan patterns to detect counterfeits:

async function analyzeVerification(unit, scanContext) {
  const flags = [];
  let result = 'authentic';

  // Get scan history from QRWorks
  const analytics = await fetch(
    `${BASE_URL}/v1/analytics/${unit.qr_analytics_id}`,
    { headers: { 'X-API-Key': API_KEY } }
  ).then(r => r.json());

  // Flag 1: Multiple scans (genuine products usually scan once)
  if (analytics.total_scans > 5) {
    flags.push('excessive_scans');
    result = 'suspect';
  }

  if (analytics.total_scans > 20) {
    flags.push('likely_counterfeit');
    result = 'counterfeit';
  }

  // Flag 2: Scans from unexpected region
  const expectedMarkets = {
    'US': ['United States', 'Canada'],
    'EU': ['Germany', 'France', 'Italy', 'Spain', 'Netherlands', 'Belgium'],
    'APAC': ['Japan', 'South Korea', 'Singapore', 'Australia']
  };

  const validCountries = expectedMarkets[unit.intended_market] || [];

  if (!validCountries.includes(scanContext.country)) {
    flags.push('wrong_region');
    if (result === 'authentic') result = 'suspect';
  }

  // Flag 3: Multiple countries (gray market or counterfeit)
  const uniqueCountries = new Set(
    analytics.scans.map(s => s.country)
  );

  if (uniqueCountries.size > 2) {
    flags.push('multi_country_scans');
    result = 'suspect';
  }

  // Flag 4: Rapid successive scans (counterfeiter testing)
  if (analytics.scans.length >= 2) {
    const lastTwo = analytics.scans.slice(0, 2);
    const timeDiff = new Date(lastTwo[0].scanned_at) -
                     new Date(lastTwo[1].scanned_at);

    if (Math.abs(timeDiff) < 60000) { // Less than 1 minute
      flags.push('rapid_scans');
    }
  }

  // Flag 5: Different device types suggests shared code
  const deviceTypes = new Set(
    analytics.scans.map(s => s.device_type)
  );

  if (deviceTypes.size > 3) {
    flags.push('multiple_device_types');
    if (result === 'authentic') result = 'suspect';
  }

  // Prepare response
  const product = await getProduct(unit.product_id);

  return {
    result,
    product: {
      name: product.name,
      sku: product.sku,
      serialNumber: unit.serial_number
    },
    verification: {
      scanNumber: analytics.total_scans + 1,
      firstScanDate: unit.first_verified_at,
      intendedMarket: unit.intended_market,
      scannedFrom: scanContext.country
    },
    flags,
    message: getResultMessage(result, flags)
  };
}

function getResultMessage(result, flags) {
  if (result === 'authentic') {
    return 'This product is authentic. Thank you for verifying.';
  }

  if (result === 'suspect') {
    if (flags.includes('wrong_region')) {
      return 'This product may be from an unauthorized seller. Contact us for verification.';
    }
    if (flags.includes('excessive_scans')) {
      return 'This code has been scanned multiple times. The product may be counterfeit.';
    }
    return 'We could not fully verify this product. Please contact support.';
  }

  return 'Warning: This product appears to be counterfeit. Do not use it.';
}

Customer-facing verification page

Show a clear result to customers:

app.get('/check/:serial/:signature', async (req, res) => {
  // ... verification logic from above ...

  // Render customer-friendly page
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Product Verification - ${product.name}</title>
      <style>
        body {
          font-family: -apple-system, sans-serif;
          max-width: 400px;
          margin: 0 auto;
          padding: 20px;
        }
        .result-card {
          border-radius: 12px;
          padding: 30px;
          text-align: center;
          margin: 20px 0;
        }
        .authentic {
          background: linear-gradient(135deg, #d4edda, #c3e6cb);
          border: 2px solid #28a745;
        }
        .suspect {
          background: linear-gradient(135deg, #fff3cd, #ffeeba);
          border: 2px solid #ffc107;
        }
        .counterfeit {
          background: linear-gradient(135deg, #f8d7da, #f5c6cb);
          border: 2px solid #dc3545;
        }
        .icon { font-size: 48px; margin-bottom: 15px; }
        .product-name { font-size: 18px; font-weight: bold; }
        .serial { font-size: 12px; color: #666; margin-top: 5px; }
        .details { margin-top: 20px; text-align: left; font-size: 14px; }
      </style>
    </head>
    <body>
      <div class="result-card ${analysis.result}">
        <div class="icon">
          ${analysis.result === 'authentic' ? '✓' :
            analysis.result === 'suspect' ? '⚠' : '✗'}
        </div>
        <div class="product-name">${product.name}</div>
        <div class="serial">Serial: ${unit.serial_number}</div>
        <p>${analysis.message}</p>
      </div>

      <div class="details">
        <p><strong>Verification #${analytics.total_scans + 1}</strong></p>
        ${unit.first_verified_at ?
          `<p>First verified: ${formatDate(unit.first_verified_at)}</p>` :
          `<p>This is the first verification.</p>`
        }
        <p>Scanned from: ${scanContext.country}</p>
        <p>Intended market: ${unit.intended_market}</p>
      </div>

      ${analysis.result !== 'authentic' ? `
        <div class="warning">
          <p><strong>Need help?</strong></p>
          <p>Contact us at verify@yourbrand.com with your serial number.</p>
        </div>
      ` : ''}
    </body>
    </html>
  `);
});

Gray market detection

Track where products are being sold outside authorized channels:

async function detectGrayMarket() {
  // Get all units with geographic mismatches
  const mismatches = await db.query(`
    SELECT
      pu.serial_number,
      pu.intended_market,
      ve.country,
      COUNT(*) as scans_from_region
    FROM product_units pu
    JOIN verification_events ve ON pu.id = ve.unit_id
    GROUP BY pu.serial_number, pu.intended_market, ve.country
    HAVING ve.country NOT IN (
      SELECT unnest(CASE pu.intended_market
        WHEN 'US' THEN ARRAY['United States', 'Canada']
        WHEN 'EU' THEN ARRAY['Germany', 'France', 'Italy', 'Spain']
        WHEN 'APAC' THEN ARRAY['Japan', 'South Korea', 'Singapore']
        ELSE ARRAY[]::text[]
      END)
    )
    ORDER BY scans_from_region DESC
  `);

  // Group by country to find hotspots
  const countryStats = {};
  for (const row of mismatches.rows) {
    if (!countryStats[row.country]) {
      countryStats[row.country] = { units: 0, scans: 0 };
    }
    countryStats[row.country].units++;
    countryStats[row.country].scans += row.scans_from_region;
  }

  return {
    mismatchedUnits: mismatches.rows,
    countryBreakdown: countryStats,
    totalGrayMarketUnits: mismatches.rows.length
  };
}

Output:

Gray Market Report
──────────────────
Total units outside intended market: 234

By Country:
China:     145 units (1,234 scans)
Russia:     52 units (456 scans)
Brazil:     37 units (189 scans)

Recent Examples:
Serial ABC123 (intended: US) → scanned 12x in China
Serial DEF456 (intended: EU) → scanned 8x in Russia

Distributor accountability

Track which distributors have products appearing in gray markets:

async function getDistributorLeakage() {
  const leakage = await db.query(`
    SELECT
      d.name as distributor_name,
      pu.intended_market,
      ve.country as leak_country,
      COUNT(DISTINCT pu.id) as leaked_units
    FROM product_units pu
    JOIN distributors d ON pu.distributor_id = d.id
    JOIN verification_events ve ON pu.id = ve.unit_id
    WHERE ve.flags @> ARRAY['wrong_region']
    GROUP BY d.name, pu.intended_market, ve.country
    ORDER BY leaked_units DESC
  `);

  return leakage.rows;
}

Output:

Distributor Leakage Report
──────────────────────────
Distributor       │ Market │ Leak To │ Units
──────────────────┼────────┼─────────┼──────
ABC Electronics   │ US     │ China   │ 89
XYZ Trading       │ EU     │ Russia  │ 45
Global Supplies   │ US     │ Brazil  │ 23

Batch manufacturing integration

Generate QR codes during production:

async function generateProductionBatch(product, batchSize, intendedMarket) {
  const units = [];
  const batchNumber = `BATCH-${Date.now()}`;

  console.log(`Generating ${batchSize} units for ${product.name}...`);

  for (let i = 0; i < batchSize; i++) {
    // Generate unique serial
    const serialNumber = `${product.sku}-${batchNumber}-${String(i).padStart(6, '0')}`;

    try {
      const unit = await createProductUnit(product, serialNumber, intendedMarket);
      units.push(unit);

      if ((i + 1) % 100 === 0) {
        console.log(`Progress: ${i + 1}/${batchSize}`);
      }

      // Rate limiting
      await new Promise(resolve => setTimeout(resolve, 50));
    } catch (error) {
      console.error(`Failed to create unit ${serialNumber}:`, error);
    }
  }

  // Generate printable sheet
  const labelSheet = generateLabelSheet(units);

  return {
    batchNumber,
    totalGenerated: units.length,
    labelSheet,
    units
  };
}

function generateLabelSheet(units) {
  // Return data for label printing software
  return {
    format: 'pdf',
    labels: units.map(u => ({
      qrCodeUrl: u.qrCodeUrl,
      serial: u.serialNumber,
      text: 'Scan to verify authenticity'
    }))
  };
}

Counterfeit alert system

Notify your team when counterfeits are detected:

async function checkForCounterfeitAlerts() {
  // Find codes with suspicious activity in last 24 hours
  const suspicious = await db.query(`
    SELECT
      pu.serial_number,
      p.name as product_name,
      ve.country,
      ve.result,
      ve.flags,
      COUNT(*) as scan_count
    FROM verification_events ve
    JOIN product_units pu ON ve.unit_id = pu.id
    JOIN products p ON pu.product_id = p.id
    WHERE ve.scanned_at > NOW() - INTERVAL '24 hours'
      AND (ve.result = 'counterfeit' OR ve.result = 'suspect')
    GROUP BY pu.serial_number, p.name, ve.country, ve.result, ve.flags
    ORDER BY scan_count DESC
  `);

  for (const alert of suspicious.rows) {
    await sendAlert({
      type: alert.result,
      product: alert.product_name,
      serial: alert.serial_number,
      country: alert.country,
      scanCount: alert.scan_count,
      flags: alert.flags
    });
  }

  return suspicious.rows;
}

async function sendAlert(alert) {
  // Send to Slack, email, or your alerting system
  const message = `
    🚨 ${alert.type.toUpperCase()} DETECTED
    Product: ${alert.product}
    Serial: ${alert.serial}
    Country: ${alert.country}
    Scans: ${alert.scanCount}
    Flags: ${alert.flags.join(', ')}
  `;

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    body: JSON.stringify({ text: message })
  });
}

Analytics dashboard queries

// Overall verification stats
async function getVerificationStats() {
  return db.query(`
    SELECT
      COUNT(*) as total_verifications,
      COUNT(*) FILTER (WHERE result = 'authentic') as authentic,
      COUNT(*) FILTER (WHERE result = 'suspect') as suspect,
      COUNT(*) FILTER (WHERE result = 'counterfeit') as counterfeit,
      COUNT(DISTINCT unit_id) as unique_products_verified
    FROM verification_events
    WHERE scanned_at > NOW() - INTERVAL '30 days'
  `).then(r => r.rows[0]);
}

// Geographic distribution
async function getVerificationsByCountry() {
  return db.query(`
    SELECT
      country,
      COUNT(*) as verifications,
      COUNT(*) FILTER (WHERE result != 'authentic') as flagged
    FROM verification_events
    WHERE scanned_at > NOW() - INTERVAL '30 days'
    GROUP BY country
    ORDER BY verifications DESC
    LIMIT 20
  `).then(r => r.rows);
}

// Time-series for detecting spikes
async function getDailyVerifications(days = 30) {
  return db.query(`
    SELECT
      DATE(scanned_at) as date,
      COUNT(*) as total,
      COUNT(*) FILTER (WHERE result = 'counterfeit') as counterfeits
    FROM verification_events
    WHERE scanned_at > NOW() - INTERVAL '${days} days'
    GROUP BY DATE(scanned_at)
    ORDER BY date
  `).then(r => r.rows);
}

Summary

You now have a product authentication system that:

  • Generates cryptographically signed verification codes
  • Validates products with a customer-friendly scan experience
  • Detects counterfeits through scan pattern analysis
  • Identifies gray market distribution via geographic data
  • Alerts your team to suspicious activity
  • Provides distributor accountability reports

The key advantage: every verification attempt is logged. Counterfeiters can copy your QR code, but they can't hide the scan patterns that reveal their fraud.


Ready to protect your products? Create your free account and start generating authentication codes.

Ready to get started?

Create your free account and start generating QR codes in minutes.