v3.0.0 is released! Automatic Transaction Splitting, Zod 4 & Precision Math.
What's New
Python v3.0.0 Pydantic v2 ValidatedPyPI Package

Python Integration Guide

omkarbhosale-upi-qr is the official Python edition of the zero-gateway UPI payment library. It brings automatic transaction splitting, Pydantic v2 schema validation with .safeParse() support, paise-level precision integer arithmetic, and complete framework parity with the JavaScript library.

What's New in Python v3.0.0

Transaction Splitting

Automatically breaks amounts > ₹2,000 into ₹1,999 intervals under NPCI guidelines.

Pydantic v2 Schemas

Strict runtime validation with full Zod parity: .safeParse() and .parse() methods.

Paise Precision Math

Operates on integer paise (round(amount * 100)) to prevent IEEE-754 float drift.

Flexible Call Signatures

Accepts keyword args, dictionary payloads, positional args, or direct Pydantic models.

Dual Import Support

Named imports, snake_case aliases, and callable upiqr object for JS developers.

Zero Third-Party Telemetry

QR codes generate completely offline in your Python process. No cloud APIs or webhooks required.

Installation

Install the latest v3.0.0 release using your package manager:

bash
pip install omkarbhosale-upi-qr
Core FeatureNPCI Optimized

1. splitTransactionQR(*args, **kwargs)

Splits large transactions exceeding a threshold (default ₹2,000) into ₹1,999 intervals and generates a QR code for each chunk concurrently. Under NPCI guidelines, transactions $\le$ ₹2,000 often bypass merchant interchange fees on PPI wallets and achieve higher checkout completion.

Function Signature

python
def splitTransactionQR(*args, **kwargs) -> List[SplitQRItem]:
# Also available as snake_case alias:
def split_transaction_qr(*args, **kwargs) -> List[SplitQRItem]:

Parameters Table

ParameterTypeRequiredDefaultDescription
UPI_ID / upi_idstrYesValid UPI ID (e.g. store@upi).
AMOUNT / amountfloat | intYesTotal amount to receive (up to ₹10,00,000).
splitInterval / split_intervalfloat | intNo1999Maximum amount per split chunk.
thresholdfloat | intNo2000Trigger threshold. If amount ≤ threshold, only 1 QR is generated.
namestrNoNonePayee name (pn parameter in UPI URI).
notestrNoNoneTransaction memo. Each chunk appends (Part X/Y).
currencystrNo"INR"Currency code. Defaults to INR.

Basic Split Example (₹5,000)

split_example.py
from omkarbhosale_upi_qr import splitTransactionQR

# Automatically splits ₹5,000 into ₹1,999 + ₹1,999 + ₹1,002
splits = splitTransactionQR({
    "UPI_ID": "store@upi",
    "AMOUNT": 5000,
    "name": "Omkar Store",
    "note": "Order #9821"
})

for item in splits:
    print(f"ID: {item.id} | Amount: ₹{item.amount}")
    # Supports both attribute access (.image) and dictionary indexing (['image'])
    print(f"Data URL: {item.image[:35]}...\n")

Expected JSON Output Structure

json
[
  {
    "id": "4a236fad-ebc5-471f-86ab-86a4f9e621e7",
    "amount": 1999,
    "image": "data:image/png;base64,iVBORw0KGgo..."
  },
  {
    "id": "c19ba6ae-664a-47d7-9948-bf3558fefb6d",
    "amount": 1999,
    "image": "data:image/png;base64,iVBORw0KGgo..."
  },
  {
    "id": "3c44b4ad-44e7-414f-86c4-60ae9f228ec6",
    "amount": 1002,
    "image": "data:image/png;base64,iVBORw0KGgo..."
  }
]

Custom Threshold & Interval Handling

custom_split.py
from omkarbhosale_upi_qr import splitTransactionQR

# 1. Custom threshold & interval
splits = splitTransactionQR(
    UPI_ID="store@upi",
    AMOUNT=1000,
    threshold=500,     # Trigger splitting on amounts > ₹500
    splitInterval=400, # Chunk in ₹400 intervals
    name="Omkar Store",
    note="Order #12345"
)
# Returns 3 QRs: ₹400 + ₹400 + ₹200
# Notes auto-appended: "(Part 1/3)", "(Part 2/3)", "(Part 3/3)"

# 2. Amounts within threshold (<= ₹2,000) return 1 item without splitting
single_split = splitTransactionQR(
    UPI_ID="store@upi",
    AMOUNT=1500,
)
# Returns 1 QR: [SplitQRItem(id="...", amount=1500, image="...")]
Core API

2. generateQR(*args, **kwargs)

Generates a single UPI QR code as a base64 Data URL (data:image/png;base64,...).

Function Signature

python
def generateQR(*args, **kwargs) -> str:
# Also available as snake_case alias:
def generate_qr(*args, **kwargs) -> str:

Usage Examples (Keyword, Dict, and Positional Args)

generate_qr.py
from omkarbhosale_upi_qr import generateQR

# 1. Keyword arguments with transaction metadata
qr_data_url = generateQR(
    UPI_ID="store@okhdfcbank",
    AMOUNT=750,
    name="Omkar Store",
    note="Coffee bill",
)
print(qr_data_url) # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...

# 2. Dictionary payload
qr_data_url = generateQR({
    "UPI_ID": "store@okhdfcbank",
    "AMOUNT": 750,
})

# 3. Positional arguments (Backwards-compatible)
qr_data_url = generateQR("store@okhdfcbank", 750)
Zod 4 Parity

3. Pydantic Schemas & Runtime Validation

Every input is validated before generating QR images. The Python package exposes Zod-compatible schema wrappers (.safeParse() and .parse()) alongside full Pydantic v2 models:

validation_demo.py
from omkarbhosale_upi_qr import (
    upiIdSchema,
    qrParamsSchema,
    splitQRParamsSchema,
    splitQRItemSchema,
    QRParams,
    SplitQRParams
)

# 1. Validate UPI ID directly with Zod-compatible .safeParse()
result = upiIdSchema.safeParse("invalid-upi")
if not result.success:
    print(result.error.issues[0].message)
    # Output: "Invalid UPI ID format. Expected format: username@bank"
else:
    print("Valid UPI ID:", result.data)

# 2. Pre-validate split parameters
result = splitQRParamsSchema.safeParse({
    "UPI_ID": "store@upi",
    "AMOUNT": 5000,
    "splitInterval": 1999,
})

if result.success:
    validated_params = result.data  # SplitQRParams instance
    print("Valid parameters for total amount:", validated_params.AMOUNT)
else:
    print("Validation error:", result.error.message)

# 3. Direct Pydantic model usage
typed_params = SplitQRParams(
    UPI_ID="merchant@okhdfcbank",
    AMOUNT=7500.0,
    splitInterval=1999.0,
    threshold=2000.0,
    name="Merchant Store",
    note="Invoice #88"
)

Validation Rules Reference:

  • UPI ID: Regex ^[\w.-]+@[\w.-]+$, 3 to 50 characters, auto-trimmed.
  • Single Amount: Positive, finite number ≤ ₹1,00,000.
  • Split Amount: Positive, finite number ≤ ₹10,00,000.
  • Error Format: Throws UPIValidationError (inherits from ValueError) formatted as Validation error: field: message.

4. Import Modes & Pythonic Aliases

To make integrating effortless for both Python developers and JavaScript teams porting code, multiple import styles are supported:

imports.py
# 1. Named imports (Recommended)
from omkarbhosale_upi_qr import generateQR, splitTransactionQR

# 2. Pythonic snake_case imports
from omkarbhosale_upi_qr import generate_qr, split_transaction_qr

# 3. Callable default object (JS parity)
from omkarbhosale_upi_qr import upiqr

single = upiqr(UPI_ID="user@upi", AMOUNT=500)
splits = upiqr.splitTransactionQR(UPI_ID="user@upi", AMOUNT=5000)

# 4. Import Pydantic models directly
from omkarbhosale_upi_qr import QRParams, SplitQRParams, SplitQRItem

5. Saving QR Code as a PNG File

To write the generated QR code directly to disk for PDF invoices, receipt printers, or email attachments, decode the base64 string using standard base64:

save_qr.py
import base64
from omkarbhosale_upi_qr import generateQR

# 1. Generate QR Data URL
qr_data = generateQR(
    UPI_ID="merchant@okhdfcbank",
    AMOUNT=1200,
    name="Omkar Store",
    note="Invoice #1092"
)

# 2. Strip the Data URL prefix and decode base64
if "," in qr_data:
    raw_base64 = qr_data.split(",")[1]
else:
    raw_base64 = qr_data

# 3. Save to disk as PNG image
with open("payment_qr.png", "wb") as f:
    f.write(base64.b64decode(raw_base64))

print("QR Code successfully saved to payment_qr.png")

6. FastAPI Backend Integration

Production-ready async FastAPI application featuring both single QR HTML checkout and split-transaction JSON API:

main.py
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from omkarbhosale_upi_qr import generateQR, splitTransactionQR, UPIValidationError

app = FastAPI(title="UPI Payment Gateway API")

UPI_ID = "store@okhdfcbank"

# 1. Single QR HTML Checkout page
@app.get("/checkout/single", response_class=HTMLResponse)
def get_checkout_page(order_id: str, amount: float):
    try:
        qr_data_url = generateQR(
            UPI_ID=UPI_ID,
            AMOUNT=amount,
            name="Omkar Store",
            note=f"Order #{order_id}"
        )
        return f"""
        <html>
            <head><title>Pay Order #{order_id}</title></head>
            <body style="font-family: system-ui; text-align: center; padding: 40px;">
                <h2>Scan & Pay for Order #{order_id}</h2>
                <h3>Total Amount: ₹{amount:.2f}</h3>
                <img src="{qr_data_url}" alt="UPI QR" style="width: 250px; height: 250px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);" />
                <p>Scan with GPay, PhonePe, Paytm, or BHIM</p>
            </body>
        </html>
        """
    except UPIValidationError as err:
        raise HTTPException(status_code=400, detail=str(err))

# 2. Split Transaction JSON API (for orders > ₹2,000)
@app.post("/api/checkout/split")
def create_split_checkout(order_id: str, amount: float):
    try:
        splits = splitTransactionQR(
            UPI_ID=UPI_ID,
            AMOUNT=amount,
            name="Omkar Store",
            note=f"Order #{order_id}"
        )
        return {
            "success": True,
            "order_id": order_id,
            "total_amount": amount,
            "parts_count": len(splits),
            "parts": [item.to_dict() for item in splits]
        }
    except UPIValidationError as err:
        raise HTTPException(status_code=400, detail=str(err))

7. Flask Backend Integration

Flask endpoint handling dynamic payment QR generation with automated splitting for orders > ₹2,000:

app.py
from flask import Flask, request, jsonify
from omkarbhosale_upi_qr import generateQR, splitTransactionQR, UPIValidationError

app = Flask(__name__)

MERCHANT_UPI = "store@okhdfcbank"

@app.route("/api/qr/generate", methods=["POST"])
def generate_payment_qr():
    data = request.get_json() or {}
    amount = float(data.get("amount", 0))
    mode = data.get("mode", "single") # 'single' or 'split'
    note = data.get("note", "Online Checkout")

    try:
        if mode == "split" and amount > 2000:
            splits = splitTransactionQR(
                UPI_ID=MERCHANT_UPI,
                AMOUNT=amount,
                note=note
            )
            return jsonify({
                "success": True,
                "mode": "split",
                "total_amount": amount,
                "parts": [item.to_dict() for item in splits]
            })
        else:
            qr_image = generateQR(
                UPI_ID=MERCHANT_UPI,
                AMOUNT=amount,
                note=note
            )
            return jsonify({
                "success": True,
                "mode": "single",
                "amount": amount,
                "qr_image": qr_image
            })
    except UPIValidationError as err:
        return jsonify({"success": False, "error": str(err)}), 400
    except Exception as err:
        return jsonify({"success": False, "error": "Internal server error"}), 500

if __name__ == "__main__":
    app.run(port=5000, debug=True)