🐍 PythonQR / Security

QR Code Validation in Python

Validate QR code content in Python — detect content types (URL, vCard, Wi-Fi credentials, email), block dangerous payloads such as javascript: URIs, and safely redirect users after scanning.

Also available in Node.js

1. QR content types

QR codes can encode many different data types. The IsValid API detects the type automatically and returns structured fields for each.

typeExample contentExtra fields
urlhttps://example.comurl
vcardBEGIN:VCARD…END:VCARDname, email, phone
wifiWIFI:S:MyNet;T:WPA;P:pass;;ssid, security, password
emailmailto:user@example.comemail, subject
phonetel:+48123456789phone
smssmsto:+48123456789:Hellophone, message
textPlain text content

2. API response structure

Endpoint: GET /v0/qr-code?value=…

{
  "valid": true,
  "type": "url",
  "content": "https://isvalid.dev/docs",
  "url": "https://isvalid.dev/docs"
}
{
  "valid": true,
  "type": "wifi",
  "content": "WIFI:S:OfficeNet;T:WPA2;P:hunter2;;",
  "ssid": "OfficeNet",
  "security": "WPA2"
}

3. Safe URL redirect pattern

⚠️Never redirect directly to QR code content without validation. A QR code can encode javascript:, data:, or other dangerous URI schemes.
async def safe_redirect_url(raw_content: str) -> str:
    result = await iv.qr_code(raw_content)

    if not result.valid:
        raise ValueError("Invalid QR code")

    if result.type != "url":
        raise ValueError(f"Expected URL QR, got: {result.type}")

    url = result.url or result.content

    # Only allow HTTPS
    if not url.startswith("https://"):
        raise ValueError("Only HTTPS URLs permitted")

    # Block dangerous schemes
    blocked = ("data:", "javascript:", "vbscript:", "file:")
    if any(url.lower().startswith(b) for b in blocked):
        raise ValueError(f"Blocked URL scheme")

    return url  # Safe to redirect

4. Blocking dangerous payloads

DANGEROUS_TYPES = {"text"}  # Unstructured text — inspect before acting

async def handle_qr_scan(content: str) -> dict:
    result = await iv.qr_code(content)

    if not result.valid:
        return {"action": "reject", "reason": "invalid_qr"}

    match result.type:
        case "url":
            url = result.url or result.content
            if not url.startswith("https://"):
                return {"action": "warn", "reason": "non_https_url", "url": url}
            return {"action": "redirect", "url": url}

        case "wifi":
            return {"action": "connect_wifi", "ssid": result.ssid}

        case "vcard":
            return {"action": "save_contact", "content": result.content}

        case _ if result.type in DANGEROUS_TYPES:
            return {"action": "display_only", "content": result.content[:500]}

        case _:
            return {"action": "display", "type": result.type, "content": result.content}

5. Batch validation with asyncio.gather

import asyncio
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))

async def validate_qr(content: str) -> dict:
    result = await iv.qr_code(content)

    if not result.valid:
        raise ValueError(f"Invalid QR code content: {content[:80]}")

    return result

async def safe_redirect_url(raw_content: str) -> str:
    """Validate QR content and return a safe redirect URL."""
    result = await validate_qr(raw_content)

    if result.type != "url":
        raise ValueError(f"Expected URL QR code, got: {result.type}")

    url = result.url or result.content
    if not url.startswith("https://"):
        raise ValueError("Only HTTPS URLs are allowed for redirects")

    # Block data: URIs and javascript: schemes
    blocked = ("data:", "javascript:", "vbscript:", "file:")
    if any(url.lower().startswith(b) for b in blocked):
        raise ValueError(f"Blocked URL scheme: {url[:40]}")

    return url

# Batch scan — validate multiple QR codes from a bulk upload
async def validate_batch(contents: list[str]) -> list[dict]:
    tasks = [iv.qr_code(c) for c in contents]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return [
        {
            "content": c,
            "result": r if not isinstance(r, Exception) else None,
            "error": str(r) if isinstance(r, Exception) else None,
        }
        for c, r in zip(contents, results)
    ]

# Example
async def main():
    url = await safe_redirect_url("https://isvalid.dev/docs")
    print(url)  # "https://isvalid.dev/docs"

asyncio.run(main())

6. Summary checklist

Always validate QR content before acting on it
Check result.type before routing (url / wifi / vcard)
Only allow HTTPS URLs for redirects
Block data:, javascript:, file: URI schemes
Do not auto-connect to Wi-Fi without user confirmation
Do not auto-save vCard without user confirmation
Use asyncio.gather for batch QR validation
Return 422 with reason on invalid QR input

See also

Ready to integrate?

Free tier — 1,000 requests/month. No credit card required.

Get your API key →