Product Authentication with QR Codes: Anti-Counterfeiting API
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.