payto-rl is a TypeScript library for handling Payto Resource Locators (PRLs). This library is based on the URL API and provides additional functionality for managing PRLs.
- 🐥 Small:
gzipped, distributed as minified ES modules.
- 📜 Standardized: Based on the URL Web API.
- 🏗️ Simple: Easy to implement.
- 🗂 Typed: Ships with types included.
- 🧪 Tested: Robust test coverage.
- 🌲 Tree Shaking: Zero dependencies, no side effects.
Install the payto-rl package using your package manager:
npm i payto-rlpnpm add payto-rlyarn add payto-rlHere's an example of how to use the payto-rl package:
import Payto from 'payto-rl';
// Basic payment URL
const paytoString = 'payto://xcb/cb7147879011ea207df5b35a24ca6f0859dcfb145999?amount=eur:10.01&swap=ctn';
const payto = new Payto(paytoString);
// Standard payment properties
console.log(payto.address); // 'cb7147879011ea207df5b35a24ca6f0859dcfb145999'
console.log(payto.amount); // 'eur:10.01'
console.log(payto.value); // 10.01
console.log(payto.network); // 'xcb'
console.log(payto.currency); // ['eur', 'ctn']
// Update payment amount
payto.value = 20.02;
console.log(payto.amount); // 'eur:20.02'
console.log(payto.swap); // 'ctn'
// Color customization
payto.colorBackground = 'ff0000'; // Red background (6-character hex)
payto.colorForeground = '000000'; // Black foreground
console.log(payto.colorBackground); // 'ff0000'
// ACH payment examples
const achPayto1 = new Payto('payto://ach/123456789/1234567'); // With routing number
console.log(achPayto1.routingNumber); // 123456789
console.log(achPayto1.accountNumber); // 1234567
const achPayto2 = new Payto('payto://ach/1234567'); // Account number only
console.log(achPayto2.accountNumber); // 1234567
// UPI/PIX payment examples (case-insensitive email)
const upiPayto = new Payto('payto://upi/USER@example.com');
console.log(upiPayto.accountAlias); // 'user@example.com'
const pixPayto = new Payto('payto://pix/user@EXAMPLE.com');
console.log(pixPayto.accountAlias); // 'user@example.com'
// Geo location example
const geoPayto = new Payto('payto://void/geo');
geoPayto.location = '51.5074,0.1278'; // Valid coordinates
console.log(geoPayto.void); // 'geo'
console.log(geoPayto.location); // '51.5074,0.1278'
// Plus code example
const plusPayto = new Payto('payto://void/plus');
plusPayto.location = '8FVC9G8V+R9'; // Valid plus code
console.log(plusPayto.void); // 'plus'
console.log(plusPayto.location); // '8FVC9G8V+R9'
// Bank details example (case-insensitive BIC)
const bankPayto = new Payto('payto://bic/deutdeff500');
console.log(bankPayto.bic); // 'DEUTDEFF500'
bankPayto.accountId = 'cb1958b39698a44bdae37f881e68dce073823a48a631';
console.log(bankPayto.accountId); // 'cb1958b39698a44bdae37f881e68dce073823a48a631'
console.log(bankPayto.toString()); // 'payto://bic/DEUTDEFF500/cb1958b39698a44bdae37f881e68dce073823a48a631'
// INTRA transfer example (intra-bank transfers)
const intraPayto = new Payto('payto://intra/pingchb2/cb1958b39698a44bdae37f881e68dce073823a48a631?amount=usd:20');
console.log(intraPayto.bic); // 'PINGCHB2'
console.log(intraPayto.accountNumber); // 'cb1958b39698a44bdae37f881e68dce073823a48a631'
console.log(intraPayto.amount); // 'usd:20'
console.log(intraPayto.value); // 20
// Mode example (preferred interaction mode)
const modePayto = new Payto('payto://xcb/address?mode=nfc');
console.log(modePayto.mode); // 'nfc'
modePayto.mode = 'qr';
console.log(modePayto.mode); // 'qr'
// Language/locale example
const langPayto = new Payto('payto://xcb/address?lang=en-US');
console.log(langPayto.lang); // 'en-US'
langPayto.lang = 'fr-CA';
console.log(langPayto.lang); // 'fr-CA'
langPayto.lang = 'es';
console.log(langPayto.lang); // 'es'
// Language codes must be 2-letter lowercase language codes, with optional region (2 letters, all lowercase or all uppercase)
// Valid: 'en', 'es', 'en-US', 'en-us', 'fr-CA', 'fr-ca', 'zh-CN'
// Invalid: 'EN', 'EN-US', 'en-Us', 'fr-Ca', 'invalid', 'eng', 'fra'
// Value handling examples
const numericPayto = new Payto('payto://example/address?amount=10.5');
console.log(numericPayto.value); // 10.5
console.log(numericPayto.amount); // '10.5'
const tokenPayto = new Payto('payto://example/address?amount=token:10.5');
console.log(tokenPayto.value); // 10.5
console.log(tokenPayto.amount); // 'token:10.5'
console.log(tokenPayto.asset); // 'token'
// Convert to JSON object
const jsonObj = payto.toJSONObject();
console.log(jsonObj.colorForeground); // Access typed properties
console.log(jsonObj['custom-field']); // Access custom propertiesnew Payto(paytoString: string)Creates a new Payto instance from a payto URL string.
| Property | Type | Description |
|---|---|---|
accountAlias |
string | null |
Email address for UPI/PIX payments (case-insensitive) |
accountId |
string | null |
Account identifier for bic and intra payments |
accountNumber |
number | string | null |
Account number (7-14 digits for ACH, alphanumeric for INTRA) |
address |
string | null |
Payment address |
amount |
string | null |
Payment amount with optional currency prefix |
asset |
string | null |
Asset type or contract address |
barcode |
'qr' | 'pdf417' | 'aztec' | 'code128' | null |
Barcode format |
bic |
string | null |
Bank Identifier Code (8 or 11 characters, case-insensitive) |
colorBackground |
string | null |
Background color in 6-character hex format |
colorForeground |
string | null |
Foreground color in 6-character hex format |
currency |
[string | null, string | null] |
Currency codes array [amount currency, swap asset] |
deadline |
number | null |
Payment deadline (Unix timestamp or positive integer) |
donate |
boolean | null |
Donation flag |
hash |
string |
URL hash component |
host |
string |
Complete host (hostname:port) |
hostname |
string |
Host without port (case-insensitive) |
href |
string |
Complete URL string |
iban |
string | null |
International Bank Account Number (case-insensitive) |
item |
string | null |
Item description (maximum 40 characters) |
lang |
string | null |
Language/locale code (2-letter lowercase language code with optional region: all lowercase or all uppercase, e.g., 'en', 'en-US', 'en-us', 'fr-CA') |
location |
string | null |
Location data (format depends on void type) |
message |
string | null |
Payment message |
mode |
string | null |
Preferred mode of Pass (e.g., 'qr', 'nfc') |
network |
string |
Network identifier (case-insensitive) |
organization |
string | null |
Organization name (maximum 25 characters) |
origin |
string | null |
URL origin |
password |
string |
URL password component |
pathname |
string |
URL path component |
port |
string |
URL port component |
protocol |
string |
URL protocol (always 'payto:') |
receiverName |
string | null |
Receiver's name |
recurring |
string | null |
Recurring payment details |
routingNumber |
number | null |
Bank routing number (9 digits) |
rtl |
boolean | null |
Right-to-left layout |
search |
string |
URL search component |
searchParams |
URLSearchParams |
URL search parameters |
split |
[string, string, boolean] | null |
Payment split information |
swap |
string | null |
Swap transaction details |
username |
string |
URL username component |
value |
number | null |
Numeric amount value (extracted from both simple and token:amount formats) |
void |
string | null |
Void path type (e.g., 'geo', 'plus') |
| Method | Return Type | Description |
|---|---|---|
toString() |
string |
Returns the complete payto URL string |
toJSON() |
string |
Returns a JSON string representation |
toJSONObject() |
PaytoJSON |
Returns a typed object with all properties and custom fields |
The library includes TypeScript type definitions and runtime validation for:
- Bank Identifier Codes (BIC) - 8 or 11 characters, case-insensitive
- Routing numbers (9 digits)
- Account numbers (7-14 digits)
- Email addresses (for UPI/PIX, case-insensitive)
- Geographic coordinates (up to 9 decimal places)
- Plus codes
- Unix timestamps
- Barcode formats
- IBAN format (case-insensitive)
- Color formats (6-character hex)
- Language/locale codes (2-letter lowercase language codes with optional region codes: region must be all lowercase or all uppercase, mixed case rejected)
Supports two formats (case-insensitive BIC):
payto://bic/bicpayto://bic/bic/account-id
Example: payto://bic/deutdeff500/cb1958b39698a44bdae37f881e68dce073823a48a631
- BIC is validated and converted to uppercase
- Optional account IDs are exposed as both
accountIdandaccountNumber - Useful for attaching a beneficiary account or CORE ID directly to the BIC route
Supports two formats (case-insensitive):
payto://iban/iban(without BIC)payto://iban/bic/iban(with BIC)
Supports two formats:
payto://ach/routing/account(with routing number)payto://ach/account(account number only)
Email-based payment identifiers (case-insensitive):
payto://upi/email@example.compayto://pix/email@example.com
Intra-bank transfers with BIC and flexible account numbers:
payto://intra/bic/account(BIC and alphanumeric account number)
Example: payto://intra/pingchb2/cb1958b39698a44bdae37f881e68dce073823a48a631?amount=usd:20
- BIC is validated and converted to uppercase
- Account numbers can be alphanumeric strings (no length restriction)
- Supports all standard payment parameters (amount, message, etc.)
This project is licensed under the CORE License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit issues and pull requests.
If you find this project useful, please consider supporting it:
Use payto://qr/{country}/{identifier} with lowercase ISO country codes. PayQR is global; Pay QR is the website's label. Current mappings are BN tarusQR, KH KHQR, ID QRIS, LA LaoQR, MY DuitNow QR, MM MMQR, PH QR Ph, SG PayNow/SGQR, TH PromptPay/Thai QR and VN VietQR.
See PayQR URI API, field coverage and validation limits. A PayTo URI, national payment payload, QR image and payment-network connection are separate things. Generating a syntactically valid QR does not imply participation in or authorization to acquire transactions from a payment network. Unsupported national profiles fail closed.
PayQR support encodes/decodes URI and structured payment data only. National QR payload generation and rendering are owned by the PayTo website.
Canonical destination: payto://qr/ph/{identifier}; network qr, country ph, scheme qrph.
The identifier is an opaque issuer-provided value, not a verified bank account, mobile number or merchant ID.
The existing generic target stores optional payment-mode=p2p|p2m and qr-type=static|dynamic in its parameters.
Payto.paymentMode and Payto.qrType expose validated getters/setters and JSON object properties.
payment-mode is a PayTo extension; mode remains the existing pass presentation property.
Example (illustrative URI, not an official payment destination or native test vector):
payto://qr/ph/Demo%40Issuer?amount=PHP%3A15.25&payment-mode=p2m&qr-type=dynamic&reference=Bill%201
PHP amounts use positive decimal strings with at most two decimal places and the existing 13-character amount limit. No floating-point conversion is used by the QR URI codec. Dynamic requests require an amount in this supported PayTo profile. Static data does not imply a recurring debit mandate. We preserve generic receiver-name/reference fields without claiming a verified native tag mapping. We do not impose participant-specific transaction limits or infer recipient requirements from another country.
Native QR Ph payload encoding and decoding are not implemented: researched public sources did not establish the Philippine account templates, routing identifiers and official conformance vectors. A PayTo URI is not a QR Ph payload accepted by banking apps. No generic EMV payload is classified as QR Ph merely from PH/PHP.
Architecture, when a verified native profile becomes available:
PayTo URI → structured payment data → native encoder → payload string
scanned payload string → native decoder → structured data → PayTo URI
The native arrows above are currently unavailable for QR Ph. Libraries stop at data, never render QR images, scan cameras, enroll merchants, connect to InstaPay, transfer funds or check payment status. QR URI input is capped at 8192 characters; duplicate query keys, controls, malformed escaping and invalid amounts are rejected.
Sources: BSP QR Ph, BSP P2P FAQ, BSP P2M FAQ, PayMongo MPM API, PayMongo QR Ph acceptance. Provider API fields are evidence for supported payment concepts, not the national TLV layout.
Canonical URI: payto://qr/id/{identifier}. Network qr, country id, scheme qris, domestic currency IDR.
The path remains an opaque acquirer-issued identifier (issuer type). It is not relabeled NMID or Merchant PAN: the reviewed public material does not establish sufficient routing semantics for that mapping. Do not invent these provider-issued values.
payto://qr/id/Demo?qr-type=static
payto://qr/id/Demo?amount=IDR%3A50000&qr-type=dynamic&reference=Bill%201
These illustrative links are not native QRIS payloads. Optional qr-type=static|dynamic uses the existing qrType property. Explicit static requests reject amount; dynamic requests require it. Unspecified type preserves a portable suggested amount without generating a native transaction. Amounts are positive IDR decimal strings, at most two fractional digits, capped at IDR 10,000,000 using integer minor-unit comparison. Decimal precision is the existing portable profile, not a claim that every provider accepts fractional rupiah. Generic receiver-name and reference remain portable metadata without a verified QRIS tag mapping. No static-to-dynamic payload conversion is implemented.
inspectEmvMpm(payload) provides bounded raw ASCII EMV envelope inspection in both libraries. It checks TLV boundaries, duplicate fields, nested template boundaries and CRC, then returns fields and templates. Its JSON shape matches across TypeScript and Dart (Dart uses .toJson()). It does not return a national scheme, NMID, Merchant PAN or PayTo target and is not a QRIS compliance validator. It rejects non-ASCII input and payloads over 4096 characters; nested unknown subfields stay raw. The official Midtrans example is a fixture with published CRC A623.
Native QRIS encoding and semantic decoding remain unsupported. Full authoritative routing/template requirements were not available in the reviewed public material. ASPI documents a specification request process. Neither a correct CRC nor ID/360 proves QRIS validity. No guessed national tags, transaction identifiers or provider credentials are generated. The website uses PayTo for this country and hides the native-format switch.
PayTo URI ↔ structured portable QR data
provider payload → raw EMV inspection (no payment destination inference)
structured QRIS → native encoder → payload → website renderer [not implemented]
Libraries render no images, perform no scanning or network calls, register no merchants, issue no identifiers and perform no transfers, status checks or settlement. Static QR does not authorize recurring debits. CPM, QRIS TAP, Tuntas, cross-border routing/FX and provider connectivity are outside this implementation.
Sources: Bank Indonesia QRIS, ASPI QRIS modes, ASPI specification-request process, annual report p. 46, Midtrans official dynamic example, EMVCo QR specifications, BI cross-border QRIS.
Payto.formats returns ['payto', 'epc'] for IBAN, or ['payto', '<scheme>']
for supported Pay QR countries: khqr, laoqr, duitnow, mmqr, paynow,
promptpay, and vietqr. PayTo-only methods (including Brunei, QR Ph, and QRIS)
omit formats from object JSON; the getter returns undefined.
PayTo is always first and is the default. Capability metadata does not validate
whether the supplied fields are sufficient for a native payment format.
reference, purpose, and information are readable/writable PayTo query
extensions, also exposed in object JSON. Assign null to remove them. Unknown
query extensions remain preserved by the URI codec. formats is derived metadata,
not a query parameter; reading it never changes the PayTo link.
These libraries encode and decode PayTo links only. They do not generate or parse EPC or national payment payloads, or render barcodes. Applications implement those formats and validate their requirements separately.
Validation coverage is partial: all current Pay QR fields round-trip through the parameter map, but dedicated field accessors/top-level JSON properties and country-specific validation are not complete. See field coverage and known validation gaps.
The currency prefix in amount defines the payment currency, including USD or EUR.
Use swap for the asset to transfer or process, for example amount=usd:100&swap=ctn.
The currency tuple is [amount currency, swap asset]; the former fiat property is removed.