Turn Your Vehicle Into a Smart Earning Asset

While you’re not driving your car or bike, it can still be working for you. MOTOSHARE helps you earn passive income by connecting your vehicle with trusted renters in your city.

🚗 You set the rental price
🔐 Secure bookings with verified renters
📍 Track your vehicle with GPS integration
💰 Start earning within 48 hours

Join as a Partner Today

It’s simple, safe, and rewarding. Your vehicle. Your rules. Your earnings.

How to Validate UPI & Paytm Payment QR Codes Programmatically (Step-by-Step Guide

Great question. You can validate “payment QR codes” at two levels:

  1. Decode & structural checks (what you can do locally in code)
  2. Live verification (needs a PSP/gateway API; optional but best-practice)

Below is a practical, copy-pasteable approach you can drop into your stack (PHP/Laravel, plus quick Node/Python snippets).


1) What kinds of payment QR payloads you’ll see

  • UPI intent URI (most common; including Paytm VPAs)
    Example:
    upi://pay?pa=merchant@paytm&pn=Acme+Store&am=123.45&cu=INR&tn=Order+123
  • EMVCo / BharatQR TLV string (starts with digits)
    Example:
    000201010211...5303561540...6304ABCD (ends with CRC “63 04 XXXX”)
  • Wallet/app-specific deep links
    • Paytm: upi://pay?... or paytmmp://... or short URL like https://paytm.me/...
      (Most Paytm merchant QRs are actually UPI with a Paytm VPA such as ...@paytm.)

2) Flow to implement

A. Decode the QR image → get the raw text

  • PHP: khanamiryan/qrcode-detector-decoder (ZXing port)
  • Node: @zxing/library, jsqr
  • Python: pyzbar or opencv + zbarlight

B. Decide the type & validate structurally

  • If it starts with upi://pay → parse as URI, validate required params.
  • If it looks like TLV (digits, starts with “00 02 01 02 11 …” etc.) → parse TLV and verify CRC16-CCITT in tag 63.
  • If it’s app link (paytmmp:// or https://paytm.me/...) → allowlist the scheme/domain and (optionally) resolve server-side to ensure it’s a valid, reachable link owned by Paytm.

C. (Optional but recommended) Live verification

  • You cannot guarantee a VPA exists or is active from the QR alone.
  • Use a PSP/gateway that exposes VPA verification (aka “UPI PSP lookup”) APIs:
    • Razorpay, Cashfree, Paytm for Business, PhonePe, Juspay, etc.
    • Typically you send pa=merchant@bank to an endpoint → they reply if it’s valid and sometimes return the account holder name.

3) Validation rules (UPI intent)

For upi://pay links:

  • Scheme must be upi, host must be pay
  • Required:
    • pa (VPA): ^[a-zA-Z0-9.\-_]{2,}@[a-zA-Z0-9.\-_]{2,}$
    • cu (currency): usually INR
  • Common/optional: pn (payee name), am (amount), tn (note), tr (txn ref), tid, mc (merchant code), orgid, sign (signed intents)
  • If you require fixed amount QRs, ensure am is present and numeric (^\d+(\.\d{1,2})?$).
  • Reject unknown params if you want a strict policy, or allow a whitelist (pa,pn,am,tn,tr,tid,mc,cu,orgid,sign,mode).

4) Validation rules (EMVCo/BharatQR TLV)

  • TLV format: <TAG><LEN><VALUE> repeated, all numeric.
  • CRC is tag 63 with length 04. Compute CRC16-CCITT (0x1021, init 0xFFFF) over the bytes up to but not including the CRC value, then compare with the provided 4-hex checksum.
  • Presence of specific tags depends on the profile (UPI merchant, etc.). For basic acceptance, verifying a correct TLV structure + CRC already weeds out malformed codes.

5) PHP (Laravel-friendly) samples

A) Decode the QR

composer require khanamiryan/qrcode-detector-decoder
Code language: JavaScript (javascript)
use Zxing\QrReader;

function decodeQrImage(string $path): ?string {
    $qrcode = new QrReader($path); // supports JPG/PNG, etc.
    $text = $qrcode->text();
    return $text !== '' ? $text : null;
}
Code language: PHP (php)

B) Validate UPI intent URI

function isValidUpiUri(string $s): bool {
    // quick precheck
    if (!str_starts_with($s, 'upi://pay')) return false;

    $parts = parse_url($s);
    if (($parts['scheme'] ?? '') !== 'upi') return false;
    if (($parts['host'] ?? '') !== 'pay') return false;

    parse_str($parts['query'] ?? '', $q);

    // Required fields
    if (empty($q['pa']) || empty($q['cu'])) return false;

    // VPA pattern (basic)
    if (!preg_match('/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+$/', $q['pa'])) return false;

    // Currency
    if (strtoupper($q['cu']) !== 'INR') return false;

    // Amount (if present)
    if (isset($q['am']) && !preg_match('/^\d+(\.\d{1,2})?$/', $q['am'])) return false;

    // Optional: enforce allowlist of params
    $allowed = ['pa','pn','am','tn','tr','tid','mc','cu','orgid','sign','mode'];
    foreach (array_keys($q) as $k) {
        if (!in_array($k, $allowed, true)) return false; // tighten as you prefer
    }

    return true;
}
Code language: PHP (php)

C) Detect & validate EMVCo/BharatQR TLV (CRC check)

function looksLikeTlv(string $s): bool {
    return ctype_digit($s) && strlen($s) >= 8; // rough heuristic
}

function parseTlv(string $s): array {
    $i = 0; $fields = [];
    while ($i + 4 <= strlen($s)) {
        $tag = substr($s, $i, 2); $i += 2;
        $len = intval(substr($s, $i, 2)); $i += 2;
        $val = substr($s, $i, $len); $i += $len;
        $fields[$tag] = $val;
        if ($i > strlen($s)) break;
    }
    return $fields;
}

// CRC16-CCITT (0x1021), init 0xFFFF, no xorout
function crc16_ccitt(string $data): string {
    $crc = 0xFFFF;
    $bytes = unpack('C*', $data);
    foreach ($bytes as $b) {
        $crc ^= ($b << 8);
        for ($i=0; $i<8; $i++) {
            $crc = ($crc & 0x8000) ? (($crc << 1) ^ 0x1021) : ($crc << 1);
            $crc &= 0xFFFF;
        }
    }
    return strtoupper(str_pad(dechex($crc), 4, '0', STR_PAD_LEFT));
}

function isValidEmvcoWithCrc(string $s): bool {
    if (!looksLikeTlv($s)) return false;
    // Find CRC tag "63"
    $pos = strpos($s, '63');
    if ($pos === false) return false;

    // The CRC field is "63 04 XXXX"
    $crcFieldStart = $pos;
    $dataForCrc = substr($s, 0, $crcFieldStart + 4); // include "63" + "04", exclude the 4 hex CRC value
    $providedCrc = substr($s, $crcFieldStart + 4, 4);
    // Compute CRC over the raw bytes, which here are ASCII digits; this simplified check is
    // acceptable for gating malformed QRs. For production, convert TLV to actual bytes.
    $calc = crc16_ccitt($dataForCrc);
    return strtoupper($providedCrc) === $calc;
}
Code language: PHP (php)

D) Putting it together

function validatePaymentQr(string $imagePath): array {
    $raw = decodeQrImage($imagePath);
    if ($raw === null) {
        return ['ok' => false, 'type' => null, 'reason' => 'QR not readable'];
    }

    // UPI intent?
    if (isValidUpiUri($raw)) {
        return ['ok' => true, 'type' => 'upi-intent', 'payload' => $raw];
    }

    // EMVCo TLV with valid CRC?
    if (looksLikeTlv($raw) && isValidEmvcoWithCrc($raw)) {
        return ['ok' => true, 'type' => 'emvco-tlv', 'payload' => $raw];
    }

    // Paytm deep links (allowlist)
    if (str_starts_with($raw, 'paytmmp://') || str_starts_with($raw, 'https://paytm.me/')) {
        return ['ok' => true, 'type' => 'paytm-link', 'payload' => $raw];
    }

    return ['ok' => false, 'type' => null, 'reason' => 'Unknown or malformed payment QR'];
}
Code language: PHP (php)

6) Node & Python quickies

Node (decode + basic UPI check)

npm i @zxing/library
Code language: CSS (css)
const { BrowserQRCodeReader } = require('@zxing/library');

function isValidUpiUri(s) {
  try {
    const u = new URL(s);
    if (u.protocol !== 'upi:' || u.hostname !== 'pay') return false;
    const pa = u.searchParams.get('pa');
    const cu = u.searchParams.get('cu');
    if (!pa || !/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+$/.test(pa)) return false;
    if ((cu || '').toUpperCase() !== 'INR') return false;
    const am = u.searchParams.get('am');
    if (am && !/^\d+(\.\d{1,2})?$/.test(am)) return false;
    return true;
  } catch { return false; }
}
Code language: JavaScript (javascript)

Python (pyzbar)

pip install pyzbar pillow
from pyzbar.pyzbar import decode
from PIL import Image
from urllib.parse import urlparse, parse_qs

def is_valid_upi_uri(s: str) -> bool:
    try:
        u = urlparse(s)
        if u.scheme != 'upi' or u.netloc != 'pay': return False
        q = {k:v[0] for k,v in parse_qs(u.query).items()}
        pa = q.get('pa'); cu = (q.get('cu') or '').upper()
        if not pa or not cu: return False
        import re
        if not re.match(r'^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+$', pa): return False
        if cu != 'INR': return False
        am = q.get('am')
        if am and not re.match(r'^\d+(\.\d{1,2})?$', am): return False
        return True
    except:
        return False

def decode_qr(path: str) -> str | None:
    d = decode(Image.open(path))
    return d[0].data.decode() if d else None
Code language: JavaScript (javascript)

7) Security & product tips

  • Don’t auto-trust a decoded string just because it parses. Enforce strict allowlists (schemes, hosts, param names).
  • Sanitize & log the original payload for audit/debug.
  • Optionally: if you only accept merchant (fixed-amount) QRs, reject those missing am.
  • Live verification: integrate a PSP’s VPA validation API to confirm the VPA exists and is active before saving the QR to your database.
  • Rate-limit and virus-scan uploads; store images in a safe bucket; validate mime & dimensions.
  • UX: after upload, show the decoded details (VPA, name, amount) back to the user for confirmation.

If you want something that is widely used in production and proven to work across banks, UPI apps, and wallets like Paytm, then the industry practice is:

✅ Most Widely Accepted Implementation

  • UPI intent URI validation + (optionally) EMVCo TLV validation + VPA verification via PSP APIs.
  • Almost all real-world QR codes in India today (Paytm, PhonePe, GPay, BHIM, BharatPe, banks) follow the NPCI UPI QR Standard which is based on EMVCo QR but typically presented as a upi://pay?... intent.
  • So, in practice:
    1. Decode the QR → you’ll almost always get upi://pay?...
    2. Check required fields (pa, cu) + format (VPA regex, currency = INR)
    3. (Optional but best) Call a PSP/aggregator API to confirm the VPA is valid

🚀 Why this is the standard

  • Banks & UPI apps interoperate because NPCI mandated UPI intent URIs and EMVCo QR as the backbone.
  • Paytm merchant QRs are just UPI QR with VPAs ending in @paytm.
  • PhonePe, GPay, Amazon Pay, BHIM → all issue UPI QR codes in the same format.
  • Even BharatQR (card+UPI) is EMVCo TLV under the hood, but most merchant-facing QRs still decode to a UPI intent.

🔑 So in your system

  • Implement UPI intent URI parsing & validation (like the PHP/Python snippets I showed).
  • Treat Paytm links as a special-case allowlist (if you want to support their paytmmp:// or https://paytm.me/...).
  • If you want bulletproof validation (to avoid fake VPAs), plug into a PSP’s VPA verification API (Razorpay, Paytm for Business, Cashfree, PhonePe, Juspay all provide it).

👉 In short:
Decode QR → Validate UPI intent URI → (optional) Live VPA verification API.
That’s the widely accepted, real-world working approach used by payment apps and fintech companies.


Subscribe
Notify of
guest
0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments

Certification Courses

DevOpsSchool has introduced a series of professional certification courses designed to enhance your skills and expertise in cutting-edge technologies and methodologies. Whether you are aiming to excel in development, security, or operations, these certifications provide a comprehensive learning experience. Explore the following programs:

DevOps Certification, SRE Certification, and DevSecOps Certification by DevOpsSchool

Explore our DevOps Certification, SRE Certification, and DevSecOps Certification programs at DevOpsSchool. Gain the expertise needed to excel in your career with hands-on training and globally recognized certifications.

0
Would love your thoughts, please comment.x
()
x