vietqr-ts
v1.0.0
Published
TypeScript library for generating EMVCo-compliant VietQR data strings based on NAPAS IBFT v1.5.2 specification
Maintainers
Readme
VietQR
TypeScript library for generating, parsing, and validating EMVCo-compliant VietQR data strings based on NAPAS IBFT v1.5.2 specification.
Features
- ✅ QR Generation: Generate EMVCo-compliant VietQR strings for static and dynamic payments
- ✅ Image Encoding: Encode VietQR strings as PNG/SVG QR code images
- ✅ String Parsing: Parse VietQR strings to extract payment information
- ✅ Data Validation: Validate parsed data against NAPAS IBFT v1.5.2 specification
- ✅ Type Safety: Full TypeScript type definitions with IDE auto-completion
- ✅ Dual Format: Supports both CommonJS and ESM
- ✅ Zero Config: Works out of the box with sensible defaults
- ✅ Battle Tested: >98% test coverage with comprehensive test suites
Installation
npm install vietqr-tsQuick Start
Generate VietQR
import { generateVietQR } from 'vietqr-ts';
// Generate static QR (user enters amount)
const staticQR = generateVietQR({
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA'
});
console.log(staticQR.rawData); // VietQR string
// Generate dynamic QR (fixed amount)
const dynamicQR = generateVietQR({
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA',
amount: '50000',
message: 'Payment for invoice #123'
});Generate QR Code Image
import { generateQRImage } from 'vietqr-ts';
const result = await generateQRImage({
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA',
amount: '50000'
}, {
format: 'png',
width: 400,
errorCorrectionLevel: 'M'
});
// Node.js: Save to file
import fs from 'fs/promises';
await fs.writeFile('qr.png', result.buffer);
// Browser: Create image element
const blob = new Blob([result.buffer], { type: 'image/png' });
const url = URL.createObjectURL(blob);
document.getElementById('qr-image').src = url;
// Or use base64 data URL
document.getElementById('qr-image').src = result.dataUrl;Parse VietQR String
import { parse } from 'vietqr-ts';
const qrString = "00020101021238570010A00000072701390006970422011301234567890200208QRIBFTTA53037045405500005802VN62160812Test Payment6304XXXX";
const result = parse(qrString);
if (result.success) {
console.log('Bank Code:', result.data.bankCode); // "970422"
console.log('Account:', result.data.accountNumber); // "0123456789"
console.log('Amount:', result.data.amount); // "50000"
console.log('Message:', result.data.message); // "Test Payment"
console.log('Currency:', result.data.currency); // "704" (VND)
console.log('Country:', result.data.countryCode); // "VN"
} else {
console.error('Parse error:', result.error.message);
}Validate Parsed Data
import { parse, validate } from 'vietqr-ts';
const parseResult = parse(qrString);
if (parseResult.success) {
const validation = validate(parseResult.data, qrString);
if (validation.isValid) {
console.log('✓ Valid VietQR payment data');
// Safe to process payment
} else {
console.error('✗ Validation failed:');
validation.errors.forEach(err => {
console.error(` - ${err.field}: ${err.message}`);
});
}
if (validation.isCorrupted) {
console.warn('⚠ Data may be truncated or corrupted');
}
}API Reference
Generation API
generateVietQR(config: VietQRConfig): VietQRResult
Generate a VietQR data string from configuration.
Parameters:
config.bankBin(string): Bank identifier (BIN) - 6 digitsconfig.accountNumber(string): Bank account number (max 19 digits)config.serviceCode(string): Service code ('QRIBFTTA'for account,'QRIBFTTC'for card)config.amount(string, optional): Transaction amount in VND (for dynamic QR)config.message(string, optional): Payment description (max 500 characters)config.purpose(string, optional): Transaction purpose code (max 25 characters)config.billNumber(string, optional): Bill/invoice reference (max 25 characters)config.merchantCategory(string, optional): Merchant category code (4 digits)
Returns:
{
rawData: string; // VietQR string
qrType: 'static' | 'dynamic';
bankBin: string;
accountNumber: string;
amount?: string;
crc: string;
}Example:
const qr = generateVietQR({
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA',
amount: '50000',
message: 'Payment for invoice #123',
billNumber: 'INV-123'
});generateQRImage(config: VietQRConfig, options?: QRImageConfig): Promise<QRImageResult>
Generate a QR code image from VietQR configuration.
Parameters:
config: Same asgenerateVietQR()options.format('png' | 'svg', default:'png'): Output image formatoptions.width(number, default:300): QR code width in pixelsoptions.errorCorrectionLevel('L' | 'M' | 'Q' | 'H', default:'M'): Error correction leveloptions.margin(number, default:4): Quiet zone margin sizeoptions.color.dark(string, default:'#000000'): Dark module coloroptions.color.light(string, default:'#FFFFFF'): Light module color
Returns:
{
buffer: Buffer | Uint8Array; // Image binary data
dataUrl: string; // Base64 data URL
format: 'png' | 'svg';
width: number;
vietqr: VietQRResult; // Generated VietQR data
}Example:
const qrImage = await generateQRImage({
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA',
amount: '50000'
}, {
format: 'svg',
width: 400,
errorCorrectionLevel: 'H',
color: {
dark: '#003366',
light: '#FFFFFF'
}
});Parsing API
parse(qrString: string): ParseResult<VietQRData>
Parse a VietQR string to extract payment information.
Parameters:
qrString(string): EMV QR formatted VietQR string
Returns:
{
success: boolean;
data?: VietQRData; // Present if success = true
error?: DecodingError; // Present if success = false
}VietQRData Structure:
{
// Payment Information
bankCode: string; // Bank identifier (BIN or CITAD)
accountNumber: string; // Bank account/card number
amount?: string; // Transaction amount (optional for static QR)
currency: string; // ISO 4217 currency code (must be "704" for VND)
// Additional Data
message?: string; // Payment description
purposeCode?: string; // Transaction purpose code
billNumber?: string; // Bill/invoice reference
// QR Metadata
initiationMethod: 'static' | 'dynamic';
merchantCategory?: string; // 4-digit MCC code
countryCode: string; // ISO 3166-1 alpha-2 (must be "VN")
// Technical Fields
payloadFormatIndicator: string; // EMV QR version (must be "01")
crc: string; // CRC-16-CCITT checksum
}Example:
const result = parse(qrString);
if (result.success) {
console.log('Payment to:', result.data.accountNumber);
console.log('Amount:', result.data.amount);
} else {
console.error('Parse failed:', result.error.message);
}Validation API
validate(data: VietQRData, qrString: string): ValidationResult
Validate parsed VietQR data against NAPAS IBFT v1.5.2 specification.
Parameters:
data: Parsed VietQR data (fromparse())qrString: Original QR string (required for CRC verification)
Returns:
{
isValid: boolean; // Overall validation status
isCorrupted: boolean; // Data corruption/truncation flag
errors: ValidationError[]; // Array of validation errors
warnings?: ValidationWarning[]; // Non-critical issues
}ValidationError Structure:
{
field: string; // Field that failed validation
code: ValidationErrorCode; // Machine-readable error code
message: string; // Human-readable description
expectedFormat?: string; // Expected format/constraint
actualValue?: string; // Actual value (sanitized)
}Example:
const parseResult = parse(qrString);
if (parseResult.success) {
const validation = validate(parseResult.data, qrString);
if (!validation.isValid) {
validation.errors.forEach(error => {
console.error(`${error.field}: ${error.message}`);
});
}
if (validation.isCorrupted) {
console.warn('QR data may be corrupted');
}
}Utility Functions
Type Guards
import {
isSuccessResult,
isErrorResult,
isDynamicQR,
isStaticQR
} from 'vietqr-ts';
// Check parse result status
if (isSuccessResult(result)) {
// TypeScript knows result.data is defined
processPayment(result.data);
}
if (isErrorResult(result)) {
// TypeScript knows result.error is defined
logError(result.error);
}
// Check QR type
if (isDynamicQR(data)) {
console.log('Fixed amount:', data.amount);
}
if (isStaticQR(data)) {
console.log('User will enter amount');
}CRC Functions
import { calculateCRC, verifyCRC } from 'vietqr-ts';
// Calculate CRC for QR string (without CRC field)
const qrWithoutCRC = "00020101021238570010A00000072701390006970422011301234567890200208QRIBFTTA53037045405500005802VN62160812Test Payment6304";
const crc = calculateCRC(qrWithoutCRC);
console.log('CRC:', crc); // e.g., "ABCD"
// Verify CRC of complete QR string
const isValid = verifyCRC(qrString);
console.log('CRC valid:', isValid);Constants
import {
NAPAS_GUID,
DEFAULT_CURRENCY,
DEFAULT_COUNTRY,
DEFAULT_MCC
} from 'vietqr-ts';
console.log(NAPAS_GUID); // "A000000727"
console.log(DEFAULT_CURRENCY); // "704" (VND)
console.log(DEFAULT_COUNTRY); // "VN"
console.log(DEFAULT_MCC); // "5812" (Restaurants)Error Handling
Generation Validation Errors
VietQR validates all input data before generating QR codes. Use validateVietQRConfig() to validate configurations or catch ValidationError exceptions:
import { validateVietQRConfig, ValidationContext, ValidationError, type ValidationErrorCode } from 'vietqr-ts';
// Option 1: Validate configuration explicitly
const config = {
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA',
amount: '50000'
};
try {
validateVietQRConfig(config);
// Config is valid, proceed with generation
const qr = generateVietQR(config);
} catch (error) {
if (error instanceof ValidationError) {
console.error(`${error.field}: ${error.message}`);
console.error('Error code:', error.code);
console.error('Expected format:', error.expectedFormat);
}
}
// Option 2: Collect all validation errors (don't stop at first error)
const context = new ValidationContext();
validateVietQRConfig(config, context);
if (context.hasErrors()) {
console.error('Validation failed with errors:');
context.getErrors().forEach(error => {
console.error(` - ${error.field}: ${error.message} [${error.code}]`);
});
// Don't proceed with generation
} else {
// All validations passed
const qr = generateVietQR(config);
}
// Option 3: Handle specific error codes
try {
validateVietQRConfig(config);
} catch (error) {
if (error instanceof ValidationError) {
switch (error.code as ValidationErrorCode) {
case 'INVALID_BANK_BIN_LENGTH':
console.error('Bank BIN must be exactly 6 digits');
break;
case 'ACCOUNT_NUMBER_TOO_LONG':
console.error('Account number cannot exceed 19 characters');
break;
case 'INVALID_AMOUNT_FORMAT':
console.error('Amount must be a valid number (e.g., "50000" or "50000.50")');
break;
case 'MISSING_ACCOUNT_OR_CARD':
console.error('Must provide either accountNumber or cardNumber');
break;
default:
console.error(`${error.field}: ${error.message}`);
}
}
}Parse Errors
import { DecodingErrorType } from 'vietqr-ts';
const result = parse(qrString);
if (!result.success) {
switch (result.error.type) {
case DecodingErrorType.INVALID_FORMAT:
console.error('QR string format is invalid');
break;
case DecodingErrorType.PARSE_ERROR:
console.error('Failed to parse QR structure');
break;
default:
console.error('Unexpected error:', result.error.message);
}
}Decoding Validation Errors
import { ValidationErrorCode } from 'vietqr-ts';
validation.errors.forEach(error => {
switch (error.code) {
case ValidationErrorCode.CHECKSUM_MISMATCH:
console.error('QR data corrupted - checksum mismatch');
break;
case ValidationErrorCode.INVALID_CURRENCY:
console.error('Only VND currency is supported');
break;
case ValidationErrorCode.MISSING_REQUIRED_FIELD:
console.error(`Required field missing: ${error.field}`);
break;
case ValidationErrorCode.LENGTH_EXCEEDED:
console.error(`Field ${error.field} exceeds maximum length`);
break;
default:
console.error(`${error.field}: ${error.message}`);
}
});Validation Error Code Reference
VietQR provides machine-readable error codes for programmatic error handling. See detailed documentation in src/validators/error-codes.ts.
Required Field Errors
| Code | Description |
|------|-------------|
| MISSING_REQUIRED_FIELD | Required field is undefined, null, or empty after trimming |
| MISSING_ACCOUNT_OR_CARD | Either accountNumber or cardNumber must be provided |
| BOTH_ACCOUNT_AND_CARD | Cannot provide both accountNumber and cardNumber |
Bank Validation Errors
| Code | Description |
|------|-------------|
| INVALID_BANK_BIN | Bank BIN format or length error (general) |
| INVALID_BANK_BIN_FORMAT | Bank BIN contains non-numeric characters |
| INVALID_BANK_BIN_LENGTH | Bank BIN is not exactly 6 digits |
Account/Card Validation Errors
| Code | Description |
|------|-------------|
| INVALID_ACCOUNT_NUMBER | Account number validation error (general) |
| ACCOUNT_NUMBER_TOO_LONG | Account number exceeds 19 characters |
| INVALID_ACCOUNT_CHARACTERS | Account number contains non-alphanumeric characters |
| INVALID_CARD_NUMBER | Card number validation error (general) |
| CARD_NUMBER_TOO_LONG | Card number exceeds 19 characters |
| INVALID_CARD_CHARACTERS | Card number contains non-alphanumeric characters |
Service Code Errors
| Code | Description |
|------|-------------|
| INVALID_SERVICE_CODE | Service code must be QRIBFTTA or QRIBFTTC |
| ACCOUNT_REQUIRED_FOR_QRIBFTTA | QRIBFTTA service code requires accountNumber |
| CARD_REQUIRED_FOR_QRIBFTTC | QRIBFTTC service code requires cardNumber |
Amount Validation Errors
| Code | Description |
|------|-------------|
| INVALID_AMOUNT_FORMAT | Amount contains non-numeric characters or invalid decimal format |
| INVALID_AMOUNT_VALUE | Amount must be a positive number (> 0) |
| AMOUNT_TOO_LONG | Amount exceeds 13 characters (including decimal point) |
| INVALID_DYNAMIC_AMOUNT | Dynamic QR codes require a valid positive amount |
Currency/Country Errors
| Code | Description |
|------|-------------|
| INVALID_CURRENCY_CODE | Currency code must be "704" (Vietnamese Dong - VND) |
| INVALID_COUNTRY_CODE | Country code must be "VN" (Vietnam) |
Merchant Category Errors
| Code | Description |
|------|-------------|
| INVALID_MCC_CODE | Merchant category code validation error (general) |
| INVALID_MCC_LENGTH | Merchant category code must be exactly 4 digits |
| INVALID_MCC_FORMAT | Merchant category code must contain only numeric characters |
Additional Data Errors
| Code | Description |
|------|-------------|
| MESSAGE_TOO_LONG | Message exceeds 500 characters |
| BILL_NUMBER_TOO_LONG | Bill number exceeds 25 characters |
| INVALID_BILL_CHARACTERS | Bill number contains invalid characters (only alphanumeric, hyphen, underscore allowed) |
| PURPOSE_TOO_LONG | Purpose exceeds 25 characters |
| REFERENCE_LABEL_TOO_LONG | Reference label exceeds 25 characters |
| INVALID_REFERENCE_CHARACTERS | Reference label contains non-alphanumeric characters |
Handling Corrupted Data
VietQR gracefully handles corrupted or truncated data:
const result = parse(corruptedQRString);
if (result.success) {
const validation = validate(result.data, corruptedQRString);
if (validation.isCorrupted) {
console.warn('QR data is corrupted but partial data extracted:');
console.log('Bank:', result.data.bankCode);
console.log('Account:', result.data.accountNumber);
// Decide whether to use partial data based on business logic
const hasCriticalFields = result.data.bankCode && result.data.accountNumber;
if (hasCriticalFields) {
console.log('Critical fields present, can proceed with caution');
}
}
}TypeScript Support
VietQR is written in TypeScript and provides full type definitions:
import type {
VietQRConfig,
VietQRResult,
QRImageConfig,
QRImageResult,
VietQRData,
ParseResult,
ValidationResult,
ValidationError,
DecodingError
} from 'vietqr-ts';
// All types are exported and fully documentedBrowser Support
VietQR works in both Node.js and modern browsers:
- Node.js: 18.x or later
- Browsers: Chrome 90+, Safari 14+, Firefox 88+, Edge 90+
Browser Example
<!DOCTYPE html>
<html>
<head>
<title>VietQR Demo</title>
</head>
<body>
<div id="qr-container"></div>
<script type="module">
import { generateQRImage } from 'https://unpkg.com/vietqr-ts';
const result = await generateQRImage({
bankBin: '970422',
accountNumber: '0123456789',
serviceCode: 'QRIBFTTA',
amount: '50000',
message: 'Payment for order #123'
});
const img = document.createElement('img');
img.src = result.dataUrl;
document.getElementById('qr-container').appendChild(img);
</script>
</body>
</html>Testing
VietQR includes comprehensive test suites:
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchTest Coverage
- Overall: >98% coverage
- Unit Tests: 625+ passing tests
- Integration Tests: Complete workflow coverage
- Compliance Tests: NAPAS IBFT v1.5.2 specification validation
Performance
- QR Generation: <10ms for typical configurations
- String Parsing: <100ms for standard VietQR strings
- Validation: <50ms for typical data
- Image Encoding: <200ms for PNG, <100ms for SVG
Specification Compliance
VietQR implements:
- ✅ NAPAS IBFT v1.5.2: Vietnamese domestic payment QR specification
- ✅ EMV QR Code Specification: Tag-Length-Value (TLV) format
- ✅ ISO 4217: Currency codes (VND = 704)
- ✅ ISO 3166-1: Country codes (Vietnam = VN)
- ✅ CRC-16-CCITT: Checksum verification with polynomial 0x1021
Contributing
Contributions are welcome! Please read our Contributing Guide before submitting pull requests.
License
MIT © Binh Nguyen
Related Projects
Support
- Issues: GitHub Issues
- Documentation: API Reference
- Specification: NAPAS IBFT v1.5.2
Made with ❤️ for the Vietnamese payment ecosystem
