ScamVerify
Use Cases

Parking and QR Code Safety

Use the ScamVerify™ API to verify QR codes on parking meters, restaurant menus, and public signage before customers are redirected to malicious payment pages.

QR code fraud at parking meters and public venues is a growing national threat. In 2025, 26 million Americans were directed to malicious sites through fake QR codes. Municipalities, parking operators, and venue managers need automated tools to verify that their QR codes have not been tampered with. The ScamVerify™ URL Lookup API catches phishing, brand impersonation, and malware in decoded QR URLs before customers enter payment information.

The Problem

Fraudsters place adhesive QR code stickers over legitimate parking meter codes, restaurant menu codes, and event venue signage. When customers scan these fake codes, they are redirected to phishing sites that mimic real payment pages.

Parking Meter Fraud

In January 2024, the City of Austin, Texas discovered 29 compromised parking meters with fraudulent QR stickers. The fake codes redirected drivers to "poybyphone.com," a near-identical misspelling of the legitimate "paybyphone.com" service. The New York City Department of Transportation issued a formal advisory warning drivers about QR sticker fraud, and the Better Business Bureau published nationwide alerts.

The attack is simple and cheap. A fraudster prints stickers for under $5, applies them in seconds, and collects payment card details from every driver who scans. A single compromised meter in a busy area can capture dozens of payment cards per day.

Restaurant and Venue QR Fraud

The same technique targets restaurant table menus, event check-in kiosks, and public information boards. One restaurant chain reported $2.3 million in damage control costs after fraudulent QR stickers were found across 200 locations. The stickers redirected customers to phishing pages that collected credit card numbers under the guise of "viewing the menu" or "placing an order."

How ScamVerify™ Helps

QR codes are just URLs. The defense is straightforward: decode the QR code locally, then verify the URL through the ScamVerify™ URL Lookup API before allowing the user to navigate to it. The API checks the decoded URL against:

  • Brand impersonation detection that catches typosquatting domains like "poybyphone" vs "paybyphone"
  • Domain age analysis that flags newly registered domains (most legitimate parking services have domains registered for years)
  • Redirect chain analysis that detects URLs that bounce through multiple domains before landing on a phishing page
  • URLhaus and ThreatFox databases containing known malicious URLs and indicators of compromise
  • Google Web Risk for social engineering and malware classification
  • SSL certificate analysis to detect suspicious or mismatched certificates

Code Example: Parking App QR Verification

import jsQR from 'jsqr';
import sharp from 'sharp';

const SCAMVERIFY_API_KEY = process.env.SCAMVERIFY_API_KEY;
const BASE_URL = 'https://scamverify.ai/api/v1';

// Known-good domains for your parking operation
const TRUSTED_DOMAINS = new Set([
  'paybyphone.com',
  'parkmobile.io',
  'parkwhiz.com',
  'spothero.com',
]);

/**
 * Decode a QR code from an image buffer and verify the URL.
 * Returns a safety assessment before the user is redirected.
 */
async function verifyMeterQR(imageBuffer) {
  // Step 1: Decode QR code from the image
  const { data, info } = await sharp(imageBuffer)
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true });

  const qr = jsQR(new Uint8ClampedArray(data), info.width, info.height);

  if (!qr || !qr.data) {
    return { safe: false, reason: 'NO_QR_DETECTED', message: 'No QR code found in image.' };
  }

  const decodedUrl = qr.data;

  // Step 2: Basic URL validation
  let parsedUrl;
  try {
    parsedUrl = new URL(decodedUrl);
  } catch {
    return {
      safe: false,
      reason: 'INVALID_URL',
      message: 'QR code does not contain a valid URL.',
      decoded: decodedUrl,
    };
  }

  // Step 3: Check against trusted domain list
  const domain = parsedUrl.hostname.replace(/^www\./, '');
  const isTrustedDomain = TRUSTED_DOMAINS.has(domain);

  // Step 4: Verify URL through ScamVerify™ API
  const response = await fetch(`${BASE_URL}/url/lookup`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${SCAMVERIFY_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url: decodedUrl }),
  });

  if (!response.ok) {
    throw new Error(`ScamVerify API error: ${response.status}`);
  }

  const result = await response.json();

  // Step 5: Build safety assessment
  const brandImpersonation = result.signals.brand_impersonation?.detected || false;
  const domainAgeDays = result.signals.domain_age_days;
  const isNewDomain = domainAgeDays !== null && domainAgeDays < 30;
  const isThreatListed = result.signals.urlhaus_listed || result.signals.threatfox_listed;

  const assessment = {
    safe: result.risk_score < 40 && !brandImpersonation && !isThreatListed,
    url: decodedUrl,
    domain,
    isTrustedDomain,
    riskScore: result.risk_score,
    verdict: result.verdict,
    explanation: result.explanation,
    warnings: [],
  };

  if (brandImpersonation) {
    assessment.warnings.push(
      `This domain may be impersonating ${result.signals.brand_impersonation.brand}. ` +
      `Verify the URL carefully before entering payment information.`
    );
  }

  if (isNewDomain) {
    assessment.warnings.push(
      `This domain was registered ${domainAgeDays} day(s) ago. ` +
      `Legitimate parking services typically have well-established domains.`
    );
  }

  if (isThreatListed) {
    assessment.warnings.push(
      'This URL appears in known malware or threat intelligence databases. Do not proceed.'
    );
  }

  if (!isTrustedDomain && result.risk_score < 40) {
    assessment.warnings.push(
      'This domain is not on your trusted parking provider list. Verify with the meter operator.'
    );
  }

  return assessment;
}

// Usage in an Express-based parking app
app.post('/api/verify-meter', async (req, res) => {
  const { imageBase64 } = req.body;
  const imageBuffer = Buffer.from(imageBase64, 'base64');

  const assessment = await verifyMeterQR(imageBuffer);

  if (!assessment.safe) {
    // Log the suspicious QR for the parking operator
    await logSuspiciousMeter({
      location: req.body.meterLocation,
      url: assessment.url,
      riskScore: assessment.riskScore,
      warnings: assessment.warnings,
      timestamp: new Date().toISOString(),
    });
  }

  return res.json(assessment);
});

Restaurant Menu QR Verification

The same pattern applies to restaurant and venue QR codes. A venue management system can verify menu QR codes during setup and periodically re-scan installed codes.

import requests
import os

SCAMVERIFY_API_KEY = os.environ["SCAMVERIFY_API_KEY"]
BASE_URL = "https://scamverify.ai/api/v1"

# Your known-good menu domain
EXPECTED_DOMAIN = "yourvenue.com"


def verify_menu_qr(decoded_url: str, table_id: str) -> dict:
    """
    Verify a decoded QR code URL against the expected domain
    and ScamVerify™ threat intelligence.
    """
    from urllib.parse import urlparse

    parsed = urlparse(decoded_url)
    domain = parsed.hostname.replace("www.", "") if parsed.hostname else ""

    # Quick check: does it match the expected domain?
    domain_matches = domain == EXPECTED_DOMAIN

    # Full verification through ScamVerify™
    response = requests.post(
        f"{BASE_URL}/url/lookup",
        headers={
            "Authorization": f"Bearer {SCAMVERIFY_API_KEY}",
            "Content-Type": "application/json",
        },
        json={"url": decoded_url},
    )
    response.raise_for_status()
    result = response.json()

    is_safe = (
        result["risk_score"] < 30
        and domain_matches
        and not result["signals"].get("brand_impersonation", {}).get("detected", False)
    )

    return {
        "table_id": table_id,
        "url": decoded_url,
        "domain_matches_expected": domain_matches,
        "risk_score": result["risk_score"],
        "verdict": result["verdict"],
        "is_safe": is_safe,
        "action": "ok" if is_safe else "replace_qr_code",
        "explanation": result["explanation"],
    }


# Periodic scan of all venue QR codes
venue_qr_codes = [
    {"url": "https://yourvenue.com/menu/table-1", "table": "T1"},
    {"url": "https://yourvenue.com/menu/table-2", "table": "T2"},
    {"url": "https://y0urvenue.com/menu/table-3", "table": "T3"},  # Tampered
]

for qr in venue_qr_codes:
    result = verify_menu_qr(qr["url"], qr["table"])
    if not result["is_safe"]:
        print(f"ALERT: Table {result['table_id']} QR code compromised!")
        print(f"  URL: {result['url']}")
        print(f"  Risk score: {result['risk_score']}")
        print(f"  Action: {result['action']}")

Risk Scoring for QR Code URLs

SignalWeightDescription
Brand impersonation detectedCriticalDomain mimics a known payment provider (e.g., "poybyphone" vs "paybyphone")
Domain age under 30 daysHighLegitimate parking and payment services have long-established domains
URLhaus or ThreatFox listingCriticalURL appears in known malware or threat intelligence databases
Redirect chain (3+ hops)HighMultiple redirects often indicate a URL that is trying to evade detection
Domain not on trusted listMediumURL resolves to a domain that is not a known parking or venue provider
Google Web Risk: SOCIAL_ENGINEERINGCriticalGoogle classifies the URL as a social engineering (phishing) threat
SSL certificate mismatchMediumCertificate issued to a different domain or organization than expected
IPQS risk score above 75HighIP Quality Score indicates the domain has a high fraud reputation

When a fraudulent QR code is discovered, preserve the sticker as evidence and report it to local law enforcement. Document the meter or table location, photograph the sticker, and include the ScamVerify™ analysis results (risk score, verdict, brand impersonation details) in your report. Many jurisdictions now have specific statutes covering QR code fraud.

Best Practices for QR Code Safety

  • Maintain a known-good domain list. Every QR code your organization deploys should resolve to a domain you control or a domain you have explicitly approved. Any QR code resolving to an unknown domain is suspicious.
  • Implement pre-redirect verification. Never navigate users directly to a QR code URL. Decode the QR locally, verify the URL through the ScamVerify™ API, and show a confirmation screen with the destination domain before redirecting.
  • Flag brand-impersonation domains immediately. If the API detects brand impersonation (e.g., a domain mimicking PayByPhone or ParkMobile), treat it as confirmed fraud. Remove the QR code and alert customers who may have already scanned it.
  • Schedule periodic QR audits. For parking operators managing hundreds of meters, run a weekly scan of all deployed QR codes. Decode each code and verify the URL. Any code that no longer resolves to the expected domain has been tampered with.
  • Use tamper-evident QR materials. Print QR codes on materials that show visible damage when a sticker is applied over them. This does not prevent fraud but makes detection easier during physical inspections.
  • Display the expected domain near the QR code. Print the legitimate URL (e.g., "paybyphone.com") next to every QR code so customers can compare it to where their phone is actually navigating.

On this page