upi_sms_parser 0.1.0
upi_sms_parser: ^0.1.0 copied to clipboard
A pure-Dart library for parsing Indian UPI transaction SMS messages. Extracts amount, merchant, UPI reference, account number, balance, transaction type, and timestamp from bank SMS. Includes a multi- [...]
upi_sms_parser #
A pure-Dart library for parsing Indian UPI transaction SMS messages. No Flutter dependency — works in any Dart project (Flutter apps, server-side Dart, CLI tools, tests).
Features #
- Multi-gate SMS filter to distinguish genuine UPI/bank transaction messages from OTPs, promotional messages, balance/EMI alerts, and mobile recharge confirmations
- Extracts: amount, merchant/recipient, UPI reference number, masked bank account number, available balance, transaction type (debit/credit), and timestamp
- Handles 8 different date/time formats used across Indian banks and
UPI apps — from
"26/04/2026 at 10:38 PM"to SBI's compact"27Mar26" - Recognises sender IDs for all major Indian banks — SBI, HDFC, ICICI, Axis, Kotak, PNB, BOB, Canara, Union Bank, IDFC FIRST, RBL, IndusInd, Federal Bank, Yes Bank, and more
- Recognises UPI apps — Google Pay, PhonePe, Paytm, BHIM, Swiggy, Amazon Pay, and others
- Zero Flutter dependency — only
dart:coreis used, so this package drops cleanly into any Dart project
Installation #
Add this to your package's pubspec.yaml:
dependencies:
upi_sms_parser: ^0.1.0
Then run:
dart pub get
Quick start #
import 'package:upi_sms_parser/upi_sms_parser.dart';
void main() {
final parser = UpiParser();
const sms = 'Payment Confirmed! Your Swiggy Order #236190425176316 of '
'Rs. 356.0 was successfully paid on 26/04/2026 at 10:38 PM. '
'Txn ID: 260298464000257. Enjoy your order!';
final result = parser.parse(sms, 'TX-SWIGGY-S');
if (result != null) {
print('Amount: ₹${result.amount}'); // ₹356.0
print('Type: ${result.type.name}'); // debit
print('Merchant: ${result.merchant}'); // null (Swiggy order SMS doesn't
// use a merchant pattern this
// package recognises)
print('UPI Ref: ${result.upiRef}'); // 260298464000257
print('Date: ${result.timestamp}'); // 2026-04-26 22:38:00.000
}
}
parser.parse returns null when the SMS either (a) doesn't look like a
genuine UPI transaction (OTP, promo, recharge confirmation, ...), or
(b) looks like one but no amount could be extracted from it. Every other
field on ParsedTransaction is independently nullable —
null simply means that particular SMS didn't phrase that piece of
information in a way the package recognises.
To process a whole inbox at once, use parseAll:
final results = parser.parseAll([
{'body': 'Rs.356 debited from A/c X6299...', 'address': 'JK-SBIUPI-S'},
{'body': 'Your OTP is 123456. Do not share.', 'address': 'JK-SBIUPI-S'},
]);
// results.length == 1 — the OTP message is silently skipped
See example/example.dart for a complete,
runnable walkthrough.
Supported SMS formats #
A non-exhaustive sample of the message shapes the package recognises:
| Source | Example SMS snippet | What gets extracted |
|---|---|---|
| SBI (bank-issued) | A/C X6299 debited by 356.00 on date 26Apr26 trf to Swiggy Refno 26029846... |
amount, type, account, merchant, ref, date |
| Swiggy (app-issued) | Your Swiggy Order #236190425176316 of Rs. 356.0 ... paid on 26/04/2026 at 10:38 PM. Txn ID: 260298464000257 |
amount, type, ref, date |
| HDFC / ICICI / Axis | Rs.1,372.00 credited to A/c XX1234 on 27-04-2026 17:13. Avl Bal: Rs.12,450 |
amount, type, account, balance, date |
| PhonePe / GPay / Paytm | ₹58 paid to Mr PRADEEP KUMAR via UPI on 06-Jun-26. UPI Ref No 1234567890 |
amount, type, merchant, ref, date |
Messages that look transactional but aren't — OTPs, promotional offers,
mobile recharge confirmations, low-balance/EMI-due alerts — are rejected by
UpiSmsFilter before any field extraction is attempted.
See How the filter works below.
API Reference #
UpiParser #
The main, high-level entry point — most apps only need this class.
| Member | Description |
|---|---|
ParsedTransaction? parse(String body, String address) |
Filters then fully parses one SMS. Returns null if it isn't a genuine UPI transaction, or if the amount couldn't be extracted. |
List<ParsedTransaction> parseAll(List<Map<String, String>> messages) |
Parses a batch of {'body': ..., 'address': ...} maps and returns only the ones that parsed successfully — silently skipping the rest. |
bool isUpiTransaction(String body, String address) |
Quick yes/no filter check without running field extraction. |
ParsedTransaction #
The immutable result returned by UpiParser.parse.
| Field | Type | Notes |
|---|---|---|
amount |
double? |
Rupee amount. The only field parse treats as required — parse returns null rather than a result with amount: null. |
type |
UpiTransactionType |
debit, credit, or unknown. |
merchant |
String? |
Recipient/merchant name, e.g. "Swiggy", "Mr PRADEEP KUMAR". |
upiRef |
String? |
Bank-assigned transaction/reference ID — useful as a dedupe key. |
bankAccount |
String? |
Masked account number, e.g. "X6299", "XX6299". |
availableBalance |
double? |
Balance remaining after the transaction, if the SMS states one. |
timestamp |
DateTime |
Parsed from the SMS body; falls back to DateTime.now() if no date pattern matched. |
rawSms |
String |
The original, unmodified SMS text. |
isDebit / isCredit |
bool |
Convenience getters over type. |
UpiTransactionType #
enum UpiTransactionType { debit, credit, unknown }
debit = money left the account, credit = money arrived, unknown =
neither set of keywords was present in the SMS body.
UpiSmsFilter #
The standalone filtering gate sequence — use this directly if you want to decide "is this a real transaction SMS?" without paying for extraction.
| Member | Description |
|---|---|
static bool isUpiTransactionSms(String body, String address) |
Runs the full 5-gate check (see below). true only if every gate passes. |
static bool isPromotionalSender(String address) |
Standalone Gate-0 check: does the sender ID carry a DLT promotional prefix (AP-, CP-, DP-, EP-, FP-)? |
UpiSmsExtractor #
The standalone field-extraction functions — use these directly if you only
need one specific piece of data and don't want to run the filter or build
a full ParsedTransaction.
| Member | Returns |
|---|---|
static double? extractAmount(String sms) |
Transaction amount in rupees. |
static String? extractMerchant(String sms) |
Merchant/recipient name. |
static UpiTransactionType extractTransactionType(String sms) |
debit / credit / unknown. |
static String? extractUpiRef(String sms) |
UPI reference / transaction ID. |
static String? extractAccountNumber(String sms) |
Masked bank account number. |
static double? extractAvailableBalance(String sms) |
Balance remaining after the transaction. |
static DateTime extractTimestamp(String sms) |
Parsed transaction date/time, or DateTime.now() as a fallback. |
How the filter works #
UpiSmsFilter.isUpiTransactionSms runs an SMS through five gates, in
order, short-circuiting on the first failure. An SMS must clear all
five to be treated as a genuine transaction record:
-
Gate 0 — Promotional sender check. Rejects messages whose sender ID carries a DLT promotional prefix (
AP-,CP-,DP-,EP-,FP-). Blocks:"AP-AIRTEL-P: Get 1GB extra data..." -
Gate 1 — Money must be mentioned. The body must contain either a currency marker (
₹,Rs.,INR, "rupees") OR a plain"debited/credited by/of/with <amount>"phrase (some banks, notably SBI, omit the currency symbol entirely). Blocks:"Your delivery is on the way!"(no money mentioned at all) -
Gate 2 — Money must have actually moved. Mentioning a rupee figure isn't enough — the message needs an action word like
"debited","credited","sent","received","paid","trf to", etc. Blocks:"Your account balance is Rs.500. Maintain minimum balance."(mentions an amount, but nothing moved) -
Gate 3 — Must be traceable to a real banking rail. Either the sender ID matches a known bank/UPI/app code (SBI, HDFC, GPay, PhonePe, ...), OR the body itself carries a transaction reference (
"UPI Ref","Txn ID","NEFT","IMPS", ...). Blocks:"Hey, I paid you ₹500 for dinner, sent via phonepe!"from a personal contact — looks transactional, but isn't bank-issued and carries no reference number. -
Gate 4 — Known non-transaction phrasing blocklist. Even messages that clear gates 1–3 are rejected if they contain tell-tale OTP, recharge, balance-alert, or phishing phrases (
"otp","do not share","recharge of","minimum balance","emi due","click here", ...). Blocks:"Your OTP for UPI transaction is 123456. Do not share. Amount: Rs.500"(looks like a transaction at first glance — but it's a phishing-style OTP message borrowing transactional language)
If you only need the Gate-0 check on its own (e.g. for a lightweight
inbox-wide pre-filter), call UpiSmsFilter.isPromotionalSender directly.
Contributing #
Found a bank or UPI app whose SMS format isn't recognised? Contributions are welcome — the most common additions are:
- New bank sender codes — add the bank's short code (lower-case) to
the
bankSenderCodeslist insideUpiSmsFilter.isUpiTransactionSms(Gate 3), so its SMS pass the "is this from a real bank?" check. - New amount/merchant/balance phrasings — add a new entry to the
relevant pattern map in
UpiSmsExtractor(extractAmount,extractMerchant,extractAvailableBalance, ...). Patterns are tried in declaration order and the first match wins, so put more specific formats earlier. - New date/time formats —
UpiSmsExtractor.extractTimestamptries eight patterns in sequence; append a new one (with a comment showing a real example string) if you encounter a format none of the existing eight handle. - New non-transaction phrases — if a promotional/OTP/recharge message
is slipping through, add its tell-tale phrase (lower-case) to the
blocklistin Gate 4.
Whatever you change, please add a corresponding test to
test/upi_sms_parser_test.dart using a real (anonymised) SMS example —
that's what keeps regressions from creeping back in as the pattern lists
grow.