Guides / Auto-verify UPI payments

How to auto-verify UPI payments without screenshots

A payment screenshot proves only that someone had a screenshot. The one reliable proof that a UPI payment reached you is your own bank saying the money was credited. Automatic verification means turning that bank confirmation into “order #1042 is paid” without a person in the middle.

This guide walks through how that works, the mistakes that make naive versions unsafe, and how to wire it into a store or app.

Why UPI payments are hard to verify automatically

When a customer pays your UPI ID directly, three things know about it: the customer's bank, your bank, and the customer. Your website isn't told anything. Your bank, however, sends you an alert — usually both an SMS and an email — that looks something like:

Rs.349.07 credited to a/c XXXXXX1234 on 23-09-26 by a/c linked to
VPA customer@okaxis (UPI Ref No 526412345678). -HDFC Bank

That message contains an amount, a UPI reference number (the UTR), often the payer's UPI ID, and nothing else. It doesn't include your order number, and the payment note the customer typed is frequently missing or cut off. So the alert tells you that money arrived but not which order it was for.

Step 1: make every order's amount unique

The fix is to put the order's identity into the one field that always survives into the bank alert: the amount. When an order is created, add a small number of paise that no other open order at that price is using.

OrderPriceCustomer pays
#1042₹349.00₹349.07
#1043₹349.00₹349.12
#1044₹1,200.00₹1,200.03

Now “Rs.349.07 credited” can belong to exactly one open order. With 1–99 paise available, you can have up to 99 simultaneous open orders at the same price. Give each order a time limit (10–15 minutes works well) so paise values are released and reused.

Show the customer the exact amount prominently, and pre-fill it with a UPI intent link or QR so they don't have to type it:

upi://pay?pa=yourshop@okhdfcbank&pn=Your%20Shop&am=349.07&cu=INR&tn=1042

Step 2: get the bank alert to your system

You need your bank's credit alerts delivered somewhere software can read them. Email is the practical choice: it is structured enough to parse and it carries authentication that SMS doesn't. There are two common approaches:

  • Read a dedicated mailbox label. Create a filter in Gmail that puts your bank's alert emails under one label, then let the verification software read only that label over IMAP using an app password. This is how Milan is usually set up.
  • Forward alerts to a processing address. A mail filter forwards just the bank's alerts to an address the software receives. This needs care, because forwarding can weaken the proof that the email genuinely came from your bank.

Either way, limit what the software can see: one label or one filter, only your bank's alerts, never your whole inbox.

Step 3: parse the alert safely

Each bank words its alerts differently, and they change over time. A parser needs to extract the amount (with paise, exactly), the UPI reference and ideally the payer's UPI ID. More importantly, it must be conservative:

  • Never read a debit as a credit. “Rs.349.07 debited” must never mark an order paid. Any debit wording should disqualify the message outright.
  • When unsure, return nothing. A missed alert is a support question; a misread one gives goods away.
  • Keep full precision. Parse ₹1,200.03 with Indian comma grouping as exactly 120003 paise. Never use floating-point numbers for money.

Milan currently understands credit alert formats from HDFC Bank, ICICI Bank and SBI. Alerts it can't parse are held for review rather than guessed at.

Step 4: make sure the alert is real

This is the step most home-made versions skip, and it is the most important one. Your customer knows the exact odd amount they were asked to pay. If your system trusts any email that says “Rs.349.07 credited,” a customer can simply send one and get their order for free.

So a verification system must check that the alert genuinely came from your bank to your mailbox — for example by relying on the mail provider's SPF and DKIM authentication results, only accepting mail from the bank's alert address, and reading the alert where your bank delivered it. Never accept a payment confirmation just because it arrived at the right address.

Step 5: match and settle, exactly once

With a trusted, parsed credit in hand, find the one open order expecting exactly that amount and mark it paid, storing the UTR. Rules that prevent expensive mistakes:

  • No near misses. ₹349.05 does not match a ₹349.07 order. Close is not correct.
  • Idempotency. The same alert may arrive twice (a re-sync, a forward and a direct copy). The UTR identifies a bank credit, so the same credit must settle only once.
  • Expired slots. If an order expired and its paise value was given to a new order, a payment now belongs to the new, live order — never the dead one.
  • Unmatched goes to review. If nothing matches (usually because the customer rounded the amount), keep the credit in a review list so a person can attach it to the right order.

Step 6: tell your app with a signed webhook

Once an order is paid, your store or app needs to know immediately so it can unlock a course, confirm a booking or print a shipping label. A webhook is an HTTP POST to your server with the details. Because anyone could POST to that URL, it must be signed. Milan, for example, sends:

X-Webhook-Timestamp: 1790159472
X-Webhook-Signature: hex HMAC-SHA256 of "<timestamp>.<raw body>"

{ "type": "order.paid",
  "data": { "orderId": "1042", "amountPaid": "349.07",
            "baseAmount": "349.00", "utr": "526412345678" } }

Your server recomputes the HMAC with its secret, compares it in constant time, rejects timestamps more than a few minutes old (so a captured request can't be replayed), and only then marks the order paid. Full Node and PHP examples are in the developer docs.

Common problems and fixes

ProblemFix
Customer paid a rounded amountShow the exact amount large and pre-filled; match leftovers from the review list.
Customer paid after the order expiredKeep the credit for review and attach it by hand; lengthen the order window if it happens often.
Alerts aren't arrivingCheck the bank's email alerts are switched on and that the filter matches the exact sender address.
Two sellers share one bank accountUse one verification setup per account, so paise values don't collide.

Build it or use a tool?

Everything above can be built in-house, and the ideas are straightforward. The time goes into the details: bank format changes, email authentication, concurrency when two orders are created at once, duplicate alerts, and webhook retries. Milan Payments handles all of these for ₹499 a month, flat, with the money still going straight into your own bank account. The first month is free, and setup takes about 15 minutes.

Stop checking screenshots

Automatic UPI verification from your bank's own alerts. ₹499/month, first month free.