QRNBUEx
Elixir library for generating NBU-compliant QR codes for Ukrainian payment systems.
Generate QR codes compatible with the National Bank of Ukraine (NBU) standards for money transfers to Ukrainian IBANs. Compatible with all major Ukrainian banking apps including PrivatBank, Monobank, Sense Bank, Ukrgazbank (EcoBank), PUMB, SportBank, IZIBank, and others.
Features
- ✅ All Three NBU QR Code Versions: V001 (EPC compatible), V002 (Base64URL), V003 (Extended)
- ✅ Full Validation: IBAN checksum, tax IDs (EDRPOU/ITIN), amounts, dates
- ✅ Character Encoding: UTF-8 and CP1251 (Windows-1251) support
- ✅ Type Safety: Custom types with compile-time validation
- ✅ Comprehensive Testing: 568 tests and 110 doctests, ~90% coverage
- ✅ QR Code Rendering: Built-in PNG, SVG, and terminal output support
- ✅ Rich Documentation: Inline docs with examples and type specs
- ✅ NBU Compliant: Implements NBU Resolution No. 97 (August 19, 2025)
Installation
Add qr_nbu_ex to your list of dependencies in mix.exs:
def deps do
[
{:qr_nbu_ex, "~> 0.3.5"}
]
end
Amounts are Decimal values. decimal comes in as a dependency of qr_nbu_ex
(2.1+ and 3.x are supported), so you don't need to add it yourself.
Quick Start
Simple Payment (V001 - Plain Text)
{:ok, qr_data} = QRNBU.generate(:v001, %{
recipient: "ТОВ Компанія Приклад",
iban: "UA213223130000026007233566001",
recipient_code: "12345678",
purpose: "Оплата за послуги"
})
# Returns plain text QR data compatible with EPC QR codes
Payment with Amount (V002 - Base64URL)
{:ok, qr_url} = QRNBU.generate(:v002, %{
recipient: "ФОП Іваненко І.І.",
iban: "UA213223130000026007233566001",
recipient_code: "1234567890", # ITIN (10 digits for individuals)
purpose: "Оплата товарів згідно рахунку №123",
amount: Decimal.new("1500.50"),
encoding: :utf8
})
# Returns: "https://qr.bank.gov.ua/[base64url_encoded_data]"
Advanced Payment (V003 - Extended Features)
{:ok, qr_url} = QRNBU.generate(:v003, %{
recipient: "ТОВ Інтернет-магазин",
iban: "UA213223130000026007233566001",
recipient_code: "12345678",
purpose: "Оплата замовлення #ORD-2024-001",
amount: Decimal.new("2450.00"),
category_purpose: "SUPP/REGU", # ISO 20022 category
reference: "INV-2024-001",
display: "Оплата протягом 7 днів",
# Must be in the future
invoice_validity: NaiveDateTime.add(NaiveDateTime.utc_now(), 7, :day),
invoice_creation: NaiveDateTime.utc_now(),
# Only the amount stays editable in the payer's banking app
field_lock: QRNBU.FieldLock.preset(:editable_amount)
})
Version Selection Guide
| Version | Use Case | Format | Features |
|---|---|---|---|
| V001 | Simple payments, EPC compatibility | Plain text | Basic fields only |
| V002 | Modern payments | Base64URL | + References |
| V003 | Advanced payments | Base64URL | + ISO 20022, field locking, timestamps |
Field Reference
Required Fields (All Versions)
- recipient: Recipient name (1-70 characters)
- iban: Ukrainian IBAN (
UA+ 27 digits with valid checksum) - recipient_code: Tax ID - EDRPOU (8 digits) or ITIN (10 digits)
- purpose: Payment purpose (1-140 characters)
Optional Common Fields
- amount: Payment amount as
Decimal.t()(e.g.,Decimal.new("100.50")) - function: Function code (
:uct,:ict,:xct) - default::uct - encoding: Character encoding (
:utf8,:cp1251) - default::utf8
V002/V003 Additional Fields
- reference: Payment reference number (max 35 characters)
V003 Exclusive Fields
- unique_recipient_id: Unique recipient identifier (max 35 characters)
- category_purpose: ISO 20022 category (format:
CCCC/PPPP, e.g.,"SUPP/REGU") - display: Display text for QR scanner (max 140 characters)
- field_lock: Which fields the payer may edit in their banking app. Pass a
QRNBU.FieldLockstruct — a preset (:fixed_payment,:editable_amount,:editable_purpose,:flexible,:minimal) or one built withQRNBU.FieldLock.lock/2/unlock/2— or a raw bitmap integer0x0000-0xFFFF, where field N is bit N. NBU requires fields 1–5, 11 and 14–17 to always be locked, so their bits (0xC83F) are always set, raw integers included (since 0.3.4). Field 17 does not fit in 16 bits. - invoice_validity: Invoice expiration as
NaiveDateTime.t() - invoice_creation: Invoice creation timestamp as
NaiveDateTime.t() - digital_signature: Digital signature (max 1000 characters)
Validation
The library automatically validates all fields:
# Invalid IBAN checksum
{:error, message} = QRNBU.generate(:v001, %{
recipient: "Test",
iban: "UA213223130000026007233566002", # Wrong checksum
recipient_code: "12345678",
purpose: "Test"
})
# Returns: {:error, "Invalid IBAN checksum"}
# Invalid tax ID format
{:error, message} = QRNBU.generate(:v002, %{
recipient: "Test",
iban: "UA213223130000026007233566001",
recipient_code: "123", # Too short
purpose: "Test"
})
# Returns: {:error, "Tax ID must be 8 digits (EDRPOU) or 10 digits (ITIN)"}
Error Handling
All functions return {:ok, result} or {:error, reason} tuples:
case QRNBU.generate(:v003, payment_data) do
{:ok, qr_url} ->
# Success: use qr_url
Logger.info("Generated QR: #{qr_url}")
{:error, reason} ->
# Handle error
Logger.error("QR generation failed: #{reason}")
end
Character Encoding
UTF-8 (Default)
{:ok, qr} = QRNBU.generate(:v002, %{
# ... fields ...
purpose: "Оплата товарів українською",
encoding: :utf8
})
CP1251 (Windows-1251)
{:ok, qr} = QRNBU.generate(:v002, %{
# ... fields ...
purpose: "Оплата товарів",
encoding: :cp1251
})
QR Code Rendering
The library includes built-in QR code rendering functionality through QRNBU.Renderer:
Render as PNG
# Generate QR data
{:ok, qr_string} = QRNBU.generate(:v001, payment_data)
# Render as PNG binary
{:ok, png_binary} = QRNBU.Renderer.to_png(qr_string)
File.write!("payment.png", png_binary)
# With custom options
{:ok, png} = QRNBU.Renderer.to_png(qr_string,
width: 500,
error_correction: :h # :l, :m, :q, or :h
)
Render as SVG
# Render as SVG string (with default UAH logo)
{:ok, svg_string} = QRNBU.Renderer.to_svg(qr_string)
File.write!("payment.svg", svg_string)
# With custom size
{:ok, svg} = QRNBU.Renderer.to_svg(qr_string, width: 400)
# Without logo
{:ok, svg} = QRNBU.Renderer.to_svg(qr_string, logo: false)
# With custom logo (no white circle background)
{:ok, svg} = QRNBU.Renderer.to_svg(qr_string, logo: "/path/to/custom-logo.svg")
# With inline SVG logo
{:ok, svg} = QRNBU.Renderer.to_svg(qr_string, logo: {:svg, "<svg>...</svg>"})
Display in Terminal
# Display QR code in terminal
:ok = QRNBU.Renderer.to_terminal(qr_string)
# With higher error correction
:ok = QRNBU.Renderer.to_terminal(qr_string, error_correction: :h)
Convenience Functions
# Generate and save as PNG in one step
:ok = QRNBU.Renderer.save_png(:v002, payment_data, "payment.png")
# Generate and save as SVG
:ok = QRNBU.Renderer.save_svg(:v003, payment_data, "payment.svg",
width: 600,
error_correction: :q
)
# Generate and display in terminal
:ok = QRNBU.Renderer.display(:v001, payment_data)
Error Correction Levels
:l(Low) - ~7% error recovery:m(Medium) - ~15% error recovery (default):q(Quartile) - ~25% error recovery:h(High) - ~30% error recovery
Higher error correction creates larger QR codes but increases resistance to damage.
Testing
The library includes comprehensive testing:
# Run all tests
mix test
# Run tests with coverage
mix test --cover
Documentation
Full documentation is available on HexDocs.
Generate local documentation:
mix docs
open doc/index.html
References
License
MIT License - see LICENSE.md for details.
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass:
mix test - Submit a pull request
Releasing
Publishing to hex.pm is done by GitHub Actions (.github/workflows/release.yml):
- Bump
@versioninmix.exsand merge tomain - Tag that commit
v<version>and push the tag:git tag v0.3.5 && git push origin v0.3.5 - The workflow runs the tests, refuses to publish if the tag does not match
@version, then publishes the package and docs with theHEX_API_KEYrepository secret