BharatpePG
Drop-in UPI payments for any Telegram bot — pay by QR, verify by UTR, deliver automatically.
✨ What it does
A customer pays the exact amount by scanning a branded QR, sends you the 12-digit UTR, and the bot verifies it against your own BharatPe account — then runs your delivery logic automatically. No payment gateway, no 2% fee, no website.
| 💳 Collect | 🔐 Verify | 📦 Deliver |
|---|---|---|
| Branded UPI QR for the exact amount | 5 safety guards + reuse protection | on_verified hook fires once per sale |
🚀 Feature highlights
- 🧾
/payflow — amount picker → professional QR card → UTR submission → verified - 🔑
/login(OTP) — authenticate your BharatPe session from your phone; merchant ID auto-discovered - 🧮
verify_utr()— standalone one-call verification for your own custom UI - 🪝
on_verifiedhook — deliver files, group invites, license keys, or credits — automatically - 🛡️ Outage-safe — background health monitor, new payments paused while down, in-flight UTRs queued and auto-delivered on recovery
- 🗄️ Zero-setup storage — stdlib SQLite by default; Postgres is one env var
- ⌨️ Inline + reply keyboards — both, out of the box
- ☁️ Deploy anywhere — Railway, Render, or a plain VPS
📦 Installation
pip install bharatpe-pg
Postgres instead of the default SQLite file
pip install "bharatpe-pg[postgres]"
Not yet on PyPI? Install from source:
pip install git+https://github.com/AshuXD-X/BharatpePG
⚡ Quickstart
Add it to any python-telegram-bot app with three calls — your own handlers stay untouched:
from telegram.ext import Application
from bharatpe_pg import PaymentConfig, init_db, register_payment_handlers, register_admin_handlers
cfg = PaymentConfig(
upi_id = "yourname@yesbankltd",
merchant_name = "My Store",
admin_ids = [123456789], # your Telegram user ID
# merchant_id is optional — auto-discovered when the admin runs /login
)
app = Application.builder().token("BOT_TOKEN").build()
init_db(cfg) # SQLite file store — no DB server needed
register_payment_handlers(app, cfg) # /pay flow
register_admin_handlers(app, cfg) # /login + /admin
app.run_polling()
🏃 Runnable examples: minimal_bot.py (defaults) · credits_bot.py (top-up wallet) · themed_bot.py (fully rebranded UI)
🔄 How it works
flowchart LR
A([User taps 💳 Pay]) --> B[Branded UPI QR<br/>exact amount]
B --> C([User pays via<br/>any UPI app])
C --> D([Sends 12-digit UTR])
D --> E{Verify against<br/>BharatPe}
E -->|✓ all guards pass| F[Claim UTR<br/>once-only]
F --> G[[on_verified<br/>deliver product]]
E -->|session down| Q[(Queue)]
Q -.->|auto on recovery| F
🔑 The /login OTP flow
BharatPe login is phone + OTP (no password). An admin runs it once:
- Send
/login→ bot asks for the 10-digit mobile - BharatPe texts the OTP → admin types it into the chat
- Bot stores the session and auto-discovers the merchant ID (BharatPe never shows it in the UI)
The OTP is entered manually every time — the bot never reads SMS. If the session expires later, just
/loginagain.
📦 Delivering the product — the on_verified hook
Set one callback; the plugin runs it exactly once per paid order, the moment a payment verifies — instantly or after an outage. This is where you hand over the goods.
async def on_verified(bot, order):
# order = {"user_id":…, "amount":…, "order_id":…, "utr":…}
await bot.send_document(order["user_id"], open("product.pdf", "rb"))
cfg = PaymentConfig(upi_id="…", merchant_name="…", on_verified=on_verified)
🧮 Real example — a credits top-up bot
Pay ₹100 → get 100 credits (rate set by you). See
examples/credits_bot.py.
CREDIT_RATE = 1 # credits per ₹ — your rule (1, 2, 10, …)
async def on_verified(bot, order):
credits = int(order["amount"] * CREDIT_RATE)
add_credits(order["user_id"], credits) # your own balance store
await bot.send_message(order["user_id"], f"🎉 Added {credits} credits.")
💡 Use cases this unlocks: file / ebook / zip delivery · license & game keys · paid group invite links · premium feature unlocks · timed subscriptions · pay-per-use credits · donation / tip jars · reseller fulfillment pings.
🎨 Customize the UI — brand everything
Nothing is hard-coded. Colours, text, buttons, and amount presets are all config — and if you want total control, pass your own QR renderer.
QR card — colours (hex or RGB), tagline, footer, fonts:
from bharatpe_pg import PaymentConfig, QRTheme
cfg = PaymentConfig(
upi_id="…", merchant_name="GlowStore",
qr_theme=QRTheme(
bg_top="#2d1b4e", bg_bottom="#0f0524", accent="#ff6b9d",
tagline="PAY & GLOW", footer_hint="UPI only", show_dots=False,
font_bold="Montserrat-Bold.ttf", # optional custom font
),
)
Buttons + amount grid:
from bharatpe_pg import UIButtons
cfg = PaymentConfig(
upi_id="…", merchant_name="…",
amount_presets=[49, 99, 199, 499], # your own quick-pay amounts
ui=UIButtons(menu_pay="🛒 Buy", custom_label="💬 Other amount", amounts_per_row=2),
)
Every message — templates with {amount}, {order_id}, {utr}, {payer}:
from bharatpe_pg import Messages
cfg = PaymentConfig(
upi_id="…", merchant_name="…",
messages=Messages(
pay_caption="🧾 Send *₹{amount:.2f}* to the QR, then paste the UTR.\n`{order_id}`",
verified="🎉 Done! ₹{amount:.2f} received from {payer}.",
),
)
Full control — bypass the built-in card with your own renderer:
def my_qr(amount, order_id, cfg):
... # build any PNG you like
return png_bytesio # io.BytesIO
cfg = PaymentConfig(upi_id="…", merchant_name="…", qr_renderer=my_qr)
All of these are optional — omit them and you get the polished defaults.
🧮 Standalone verification — verify_utr()
Already have your own payment UI? Call one function. It runs all five guards plus reuse protection and returns a result — no need for the /pay handlers:
from bharatpe_pg import verify_utr, init_db
init_db(cfg)
r = verify_utr("664700063288", 100.0, order_id="topup-42", user_id=123, cfg=cfg)
if r.ok: # r.reason: verified | bad_utr | not_found | reused | gateway_down | error
add_credits(123, 100)
🛡️ Verification guards
Every UTR must clear all of these before an order is marked paid:
| # | Guard | Protects against |
|---|---|---|
| 1 | Exists in your BharatPe transactions | fake UTRs |
| 2 | Type PAYMENT_RECV + status SUCCESS |
refunds / failed txns |
| 3 | Amount matches the order | underpaying |
| 4 | Not already used (utr is UNIQUE) |
replaying one payment |
| 5 | Recent, within utr_window_sec |
recycling old UTRs |
⚙️ What happens during an outage
The BharatPe session expires periodically (it's a login session, not an API key). The plugin handles it so you don't have to:
- 🔭 Background monitor checks health every few minutes → DMs admins once when it expires
- 🚫 While down, new
/payis refused ("gateway down, try later") — no QR for a payment you can't verify - 📥 A UTR paid during the window is queued; on recovery it's verified and
on_verifiedfires — product delivered automatically, no resend
🛠️ Managing your bot
Everything you need after deploy is done from inside Telegram, as an admin — no server access, no BharatPe dashboard. Open /admin (admins only).
Admin controls
| Button / command | What it does |
|---|---|
/login · 🔑 Login |
Connect BharatPe: send mobile → enter the OTP from your phone. Merchant ID is auto-discovered. |
| 🔌 Status | Live connection check — 🟢 logged in · 🔴 not logged in / expired · 🟡 BharatPe unreachable. |
| 💰 Recent | Last 10 orders with status + UTR. |
| 🔍 Search | Look up a payment by order ID or UTR. |
/cancel |
Abort an in-progress login. |
Automatic vs. manual
| ✅ Automatic (you do nothing) | 🙋 Manual (you act) |
|---|---|
| Verifying UTRs when users submit them | One-time /login per session (OTP from your phone) |
Running on_verified to deliver goods/credits |
Re-login when the session expires — you're DMed first |
| Detecting session expiry (background monitor) | — |
| Pausing payments during an outage | — |
| Queuing + delivering payments after recovery | — |
| Restoring the session after a redeploy | — |
You're alerted before customers are affected: the monitor DMs admins the moment the session expires, so you re-login in ~10 seconds and nothing breaks downstream.
⚠️ Keep your session across redeploys — use durable storage
The session is saved to your storage so a redeploy doesn't force a re-login. But that only holds if the storage itself survives:
-
VPS — the default SQLite file persists on disk → nothing to do. ✅
-
Railway / Render — the default disk is wiped on redeploy, taking the SQLite file (and your session + payment records) with it. Use one of:
- a mounted volume with
DB_PATHpointing into it, or - Postgres via
DATABASE_URL.
Without durable storage you'll have to
/loginagain after every deploy, and payment history resets. - a mounted volume with
Command names clash with your bot's? Rename them.
The plugin's four commands — pay, admin, login, cancel — are configurable. If your bot already uses any of those, just rename the plugin's in PaymentConfig (give the bare word, no slash):
cfg = PaymentConfig(
upi_id="…", merchant_name="…",
cmd_pay="buy", # /buy instead of /pay
cmd_admin="panel", # /panel instead of /admin
cmd_login="bplogin",
cmd_cancel="abort",
)
Why this matters: python-telegram-bot runs only the first handler that matches a command within a group, so two handlers on the same command would shadow each other. Renaming removes the clash entirely — no shared command, no shadowing.
Free-text messages don't clash. The plugin's text handlers live in groups 1 and 2 (not the default group 0) and only act on their own state (awaiting a UTR / an admin input) — your own group-0 text handlers run normally alongside.
Prefer to use none of the plugin's commands? Skip register_admin_handlers and drive everything from your own UI with the building blocks:
from bharatpe_pg.bharatpe import start_login, complete_login, check_credentials, has_session
from bharatpe_pg.database import admin_recent, admin_search
The payment flow (register_payment_handlers) is independent of the admin panel — you can ship /pay + on_verified without the plugin's /admin at all.
🔧 Configuration
PaymentConfig is a dataclass — pass values directly or from env. Only upi_id and merchant_name are required.
| Field | Default | Meaning |
|---|---|---|
upi_id |
— (required) | Your UPI VPA, encoded into the QR |
merchant_name |
— (required) | Shown on the QR card |
admin_ids |
[] |
Telegram IDs allowed to run the admin + login commands |
on_verified |
None |
Async hook run once per paid order |
cmd_pay / cmd_admin / cmd_login / cmd_cancel |
pay / admin / login / cancel |
Rename the plugin's commands to avoid clashing with yours |
merchant_id |
"" |
Auto-discovered at /login — usually leave blank |
auth_host |
enterprise.bharatpe.in |
Login/OTP host (configurable) |
api_host |
payments-tesseract.bharatpe.in |
Transactions host (configurable) |
db_url |
"" |
Set a postgres://… DSN to use Postgres |
db_path |
payments.db |
SQLite file path (when db_url is empty) |
utr_window_sec |
1800 |
Reject UTRs older than this (30 min) |
session_check_sec |
300 |
Background health-check interval |
min_amount / max_amount |
1 / 50000 |
Allowed range (₹) |
☁️ Deploy
| Platform | Notes |
|---|---|
Run python examples/minimal_bot.py, set env vars. Mount a volume + set DB_PATH so SQLite survives redeploys, or use Postgres. |
|
Same — add a persistent disk for durable storage, or set DATABASE_URL. |
|
Run under systemd / pm2; the default SQLite file just works. |
Postgres (opt-in): pip install "bharatpe-pg[postgres]" and set DATABASE_URL=postgres://…. The same utr UNIQUE schema is created automatically — recommended for multi-process setups.
🔒 Security
- 🚫 Never commit your
.env, session files, or any Burp/HAR capture — all gitignored. - 🔑 Credentials come from the live
/loginsession, never from source. - 🧷
utr UNIQUEmakes "one UTR, one order" a storage invariant — copied UTRs can't be replayed. - 👮
/loginand/adminare locked toadmin_ids.
🧪 Tests
Four stdlib assert self-checks (no pytest, no network) guard the money-critical logic:
python tests/test_utr_reuse.py # one UTR can't pay two orders
python tests/test_queue_drain.py # outage queue verifies + delivers on recovery
python tests/test_verify_and_hook.py # verify_utr guards + on_verified fires once
python tests/test_session_persist.py # session survives a redeploy (no re-login)
MIT Licensed · Built for small Telegram sellers who want 0% UPI collection without a gateway.
Not affiliated with or endorsed by BharatPe. Use responsibly with your own merchant account.
Metadata
Release files for bharatpe-pg 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bharatpe_pg-0.1.1.tar.gz | 34.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bharatpe_pg-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.8 kB
Release files / bharatpe_pg-0.1.1.tar.gz
| Download URL | bharatpe_pg-0.1.1.tar.gz |
|---|---|
| Size | 34.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bf6aa1360f4ff489b858c608928fb4b8488cd089858693f421168f243512826f
|
|
BLAKE2b-256 checksum How to use checksums |
c46220e95b249ac6295e72790814043714751dce77265809f3af632b80098e01
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.0
|
Release files / bharatpe_pg-0.1.1-py3-none-any.whl
| Download URL | bharatpe_pg-0.1.1-py3-none-any.whl |
|---|---|
| Size | 30.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c1f4c22cc038cd0d49a181e0b45c586f01c8b39ad1ba3b839f399150caf88795
|
|
BLAKE2b-256 checksum How to use checksums |
c4a6ed4dd0454bedbfdafe59ca7a137173e8489e9f0ec08d7231d85e67886b01
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.0
|