UPI
The upi gem generates UPI payment URIs and QR codes, in PNG or SVG, that conform to the
NPCI UPI Linking Specification
(common URL specification for deep linking and proximity integration).
Installation
Add this line to your application's Gemfile:
gem 'upi'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install upi
Usage
Initialize Upi::Generator with the payee details, then call generate_qr for a QR code or
upi_content for the raw payment URI. Generator instances are immutable and safe to reuse
and share across threads.
Individual mode (P2P)
require 'upi'
generator = Upi::Generator.new(
upi_id: 'test@upi',
name: 'Test Name'
)
Do not pass merchant_code for individual payments; it is omitted entirely.
QR code in SVG format
svg_content = generator.generate_qr(100, 'Personal Payment', mode: :svg)
File.write('qr_code.svg', svg_content)
QR code in PNG format
png_content = generator.generate_qr(100, 'Personal Payment', mode: :png)
File.binwrite('qr_code.png', png_content)
Payment URI
upi_content returns a UPI URI you can use as the href of a "Pay Now" link:
payment_url = generator.upi_content(100, 'Personal Payment')
# => "upi://pay?pa=test@upi&pn=Test%20Name&tn=Personal%20Payment&am=100.00&cu=INR"
<a href="<%= payment_url %>">Pay Now</a>
Letting the payer choose the amount
Omit the amount and the am tag is left out, so the payer types their own. Never pass 0 for
this — UPI apps reject am=0, and the gem raises rather than emit it.
generator.upi_content
# => "upi://pay?pa=test@upi&pn=Test%20Name&cu=INR"
Merchant mode (P2M)
Pass a four-digit ISO 18245 merchant category code:
generator = Upi::Generator.new(
upi_id: 'merchant@upi',
name: 'Test Name',
merchant_code: '5411'
)
A merchant payment that carries an amount must also carry a transaction reference (tr),
and that reference must be unique for every payment attempt. PSPs reject a reference they
have already seen as a duplicate, so a QR code with a hard-coded reference works once and then
silently stops. Upi::Generator.generate_reference builds a suitable one:
reference = Upi::Generator.generate_reference('ORD') # => "ORD20260917131656E4BBC91A"
svg_content = generator.generate_qr(
500, 'Payment for Goods',
transaction_ref_id: reference,
transaction_id: 'TXN456',
url: 'https://merchant.com/payment',
mode: :svg
)
In practice the reference is your own order or invoice number, which is what makes reconciliation work:
generator.upi_content(500, 'Payment for Goods', transaction_ref_id: "ORD#{order.id}")
Leaving the amount out produces a static "shop counter" QR, which needs no reference because the payer supplies a fresh one each time:
generator.upi_content
# => "upi://pay?pa=merchant@upi&pn=Test%20Name&mc=5411&cu=INR"
Letting the payer pay at least a minimum
min_amount maps to the mam tag: the amount becomes editable, down to that floor.
generator.upi_content(500, 'Donation', transaction_ref_id: 'ORD90211', min_amount: 100)
# => "upi://pay?pa=merchant@upi&pn=Test%20Name&mc=5411&tr=ORD90211&tn=Donation&am=500.00&mam=100.00&cu=INR"
Parameters
Upi::Generator.new
| Parameter | Required | Notes |
|---|---|---|
upi_id: |
yes | Payee VPA, name@psp. Max 255 characters. |
name: |
yes | Payee name shown on the payer's confirmation screen. Max 99 characters. |
currency: |
no | Defaults to 'INR', the only value UPI supports. |
merchant_code: |
no | Four-digit ISO 18245 category code. Its presence selects merchant mode. |
no_url_parse: |
no | Encoding strictness, see Encoding. Defaults to true. |
#upi_content and #generate_qr
Both take the same payment arguments; generate_qr adds the rendering ones.
| Parameter | Notes |
|---|---|
amount |
First positional. Omit to let the payer choose. Max two decimal places. |
note |
Second positional. Maps to tn. Max 50 characters. |
transaction_ref_id: |
tr. Mandatory for merchant payments with an amount. Max 35 alphanumeric characters. |
transaction_id: |
tid, PSP-generated when present. Max 35 alphanumeric characters. |
url: |
http(s) URL with further transaction detail. |
min_amount: |
mam. Makes the amount editable down to this floor. |
initiation_mode: |
mode. Opt-in, see Initiation mode. |
mode: |
generate_qr only. :svg (default) or :png. |
level: |
generate_qr only. QR error correction: :l, :m (default), :q, :h. |
Any other keyword to generate_qr is passed through to the renderer, so
module_px_size:, border_modules:, color: and the rest of the
rqrcode options still work:
generator.generate_qr(100, 'Tea', mode: :png, module_px_size: 12, color: '333')
Amounts
Amounts are normalised to the two-decimal format UPI requires, and anything that cannot be represented that way raises rather than producing an unpayable QR code.
generator.upi_content(100) # => "...&am=100.00&..."
generator.upi_content(1499.5) # => "...&am=1499.50&..."
generator.upi_content(0.1 + 0.2) # => "...&am=0.30&..." float noise is discarded
generator.upi_content(BigDecimal('99.9'))
generator.upi_content(100.456) # raises Upi::ValidationError, more than two decimals
generator.upi_content(0) # raises Upi::ValidationError, UPI apps reject am=0
Pass a String or BigDecimal when the amount comes from a currency column, so no float ever
touches it.
Validation
Every field is checked against the specification before a URI is built, and a failure raises
Upi::ValidationError (a subclass of Upi::Error). This is deliberate: an invalid UPI URI
still produces a perfectly scannable QR code, so without validation the only symptom is a
payment that fails later, in the payer's app.
Upi::Generator.new(upi_id: 'not-a-vpa', name: 'X')
# Upi::ValidationError: upi_id "not-a-vpa" is not a valid UPI address (expected 'name@psp')
Encoding
Values are percent-encoded per RFC 3986, with spaces as %20, matching the example URIs in
the specification. no_url_parse selects how aggressive that is:
true(default) encodes down to the unreserved set.falseleaves URL-safe punctuation such as:and/readable.
Either way the characters that would corrupt the query string — &, #, %, + and space —
are always escaped, so a note like Tea & Coffee #12 can never truncate the URI or forge
another parameter. Redirect URLs keep their :// readable in both modes.
Initiation mode
The specification lists a mode tag (01 for QR, 04 for intent). It is left out by default,
since most PSP apps accept a URI without it and existing integrations rely on that. Opt in when
you want it:
generator.upi_content(500, 'Goods', transaction_ref_id: 'ORD1',
initiation_mode: Upi::Generator::MODE_QR)
# => "...&cu=INR&mode=01"
Signed QR codes (mode=02, requiring the sign and orgid tags) are not supported.
Upgrading from 2.x
3.0 changes the generated URI so that it complies with the specification. See the CHANGELOG for the full list. The changes most likely to affect you:
- Spaces encode as
%20, not+, and parameters are emitted in the specification's order, so any test asserting an exact URI string needs updating. - Amounts always carry two decimals:
am=100is nowam=100.00. - Calling without an amount no longer emits
am=0&tn=; both tags are omitted. - Invalid input now raises
Upi::ValidationErrorinstead of silently producing a broken QR code. Merchant payments with an amount now requiretransaction_ref_id. #paramsreturns the frozen constructor parameters rather than the last call's state.- PNG output is sized by module rather than pinned to 300px, so images vary in size with the
payload. Pass
size:ormodule_px_size:to control it.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/stingrayzboy/upi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Upi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.