Overview
This guide will walk you through creating your first Dynamic QR Ph payment in under 10 minutes. By the end, you’ll have generated a QR code that customers can scan to make a payment.
This quickstart uses Node.js , but the same principles apply to any language. Check the API Reference for examples in other languages.
Prerequisites
Get Your Credentials
Contact Modulus Labs to receive:
Secret Key (starts with sk_)
Encryption Key
Activation Code (format: XXXX-XXXX-XXXX-XXXX)
Install Node.js
Ensure you have Node.js 14+ installed node --version
# v18.0.0 or higher
Create a New Project
mkdir qr-api-demo
cd qr-api-demo
npm init -y
Step 1: Install Dependencies
Install the required packages:
npm install axios jose dotenv
Package Purposes:
axios - HTTP client for API requests
jose - JWE token encryption/decryption
dotenv - Manage environment variables securely
Step 2: Set Up Environment Variables
Create a .env file in your project root:
MODULUS_SECRET_KEY = sk_YOUR_SECRET_KEY_HERE
MODULUS_ENCRYPTION_KEY = YOUR_ENCRYPTION_KEY_HERE
MODULUS_ACTIVATION_CODE = XXXX-XXXX-XXXX-XXXX
Never commit your .env file to version control!
Add it to .gitignore immediately:echo ".env" >> .gitignore
Note:
The MODULUS_ENCRYPTION_KEY must be exactly 32 characters to match the A256KW encryption key-length requirement. Any leading/trailing whitespace will cause encryption to fail.
Step 3: Create Your First QR Code
Create create-qr.js:
require ( 'dotenv' ). config ();
const axios = require ( 'axios' );
const jose = require ( 'jose' );
const fs = require ( 'fs' );
const crypto = require ( 'crypto' );
const SECRET_KEY = process . env . MODULUS_SECRET_KEY ;
const ENCRYPTION_KEY = process . env . MODULUS_ENCRYPTION_KEY ;
const ACTIVATION_CODE = process . env . MODULUS_ACTIVATION_CODE ;
async function createQRPayment ( amount , reference ) {
console . log ( ' Creating Dynamic QR Ph... \n ' );
try {
// Step 1: Prepare payload
console . log ( ' Step 1: Preparing payment data...' );
const payload = {
activationCode: ACTIVATION_CODE ,
currency: 'PHP' ,
amount: amount . toFixed ( 2 ),
merchantReferenceNumber: reference || crypto . randomUUID ()
};
console . log ( 'Payload:' , payload );
// Step 2: Encrypt payload
console . log ( ' \n Step 2: Encrypting payload...' );
const key = Buffer . from ( ENCRYPTION_KEY , 'utf-8' );
const jweToken = await new jose . CompactEncrypt (
new TextEncoder (). encode ( JSON . stringify ( payload ))
)
. setProtectedHeader ({ alg: 'A256KW' , enc: 'A256CBC-HS512' })
. encrypt ( key );
console . log ( 'JWE Token (first 50 chars):' , jweToken . substring ( 0 , 50 ) + '...' );
// Step 3: Make API request
console . log ( ' \n Step 3: Calling API...' );
const response = await axios . post (
'https://qrph.sbx.moduluslabs.io/v1/pay/qr' ,
{ Token: jweToken },
{
auth: { username: SECRET_KEY , password: '' },
headers: { 'Content-Type' : 'application/json' }
}
);
console . log ( 'API Response received' );
// Step 4: Decrypt response
console . log ( ' \n Step 4: Decrypting response...' );
const { plaintext } = await jose . compactDecrypt (
response . data . Token ,
key
);
const result = JSON . parse ( new TextDecoder (). decode ( plaintext ));
console . log ( ' \n Success!' );
console . log ( 'Transaction ID:' , result . id );
console . log ( 'QR Body length:' , result . qrBody . length , 'characters' );
// Step 5: Save QR code
console . log ( ' \n Step 5: Saving QR code...' );
const buffer = Buffer . from ( result . qrBody , 'base64' );
const filename = `qr- ${ result . id } .png` ;
fs . writeFileSync ( filename , buffer );
console . log ( `QR code saved as: ${ filename } ` );
console . log ( ' \n Done! Show this QR code to customers for payment.' );
return result ;
} catch ( error ) {
console . error ( ' \n Error creating QR code:' );
if ( error . response ) {
console . error ( 'Status:' , error . response . status );
console . error ( 'Data:' , error . response . data );
// Try to decrypt error if it's a JWE token
if ( error . response . data . Token ) {
try {
const key = Buffer . from ( ENCRYPTION_KEY , 'utf-8' );
const { plaintext } = await jose . compactDecrypt (
error . response . data . Token ,
key
);
const errorData = JSON . parse ( new TextDecoder (). decode ( plaintext ));
console . error ( 'Error Details:' , errorData );
} catch ( decryptError ) {
console . error ( 'Could not decrypt error response' );
}
}
} else {
console . error ( 'Error:' , error . message );
}
throw error ;
}
}
// Run it!
const amount = 500.00 ; // ₱500.00
const reference = 'ORDER-' + Date . now ();
createQRPayment ( amount , reference )
. then (() => process . exit ( 0 ))
. catch (() => process . exit ( 1 ));
Run it:
Expected output:
Creating Dynamic QR Ph...
Step 1: Preparing payment data...
Payload: {
activationCode: 'A9X4-B7P2-Q6Z8-M3L5',
currency: 'PHP',
amount: '500.00',
merchantReferenceNumber: 'ORDER-1706543210123'
}
Step 2: Encrypting payload...
JWE Token (first 50 chars): eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEy...
Step 3: Calling API...
API Response received
Step 4: Decrypting response...
Success!
Transaction ID: 0ce32626-08cf-405c-adab-6d384a8871da
QR Body length: 1234 characters
Step 5: Saving QR code...
QR code saved as: qr-0ce32626-08cf-405c-adab-6d384a8871da.png
Done! Show this QR code to customers for payment.
Step 4: View Your QR Code
Open the generated PNG file:
# macOS
open qr- * .png
# Linux
xdg-open qr- * .png
# Windows
start qr- * .png
You should see a QR code image that customers can scan to make a payment.
Step 5: Test Payment (Optional)
To test the complete flow, follow the Testing Guide to simulate a payment and receive a webhook notification.
Quick Test Summary:
Simulate Payment
Use the raw QR value with the Simulate Webhook API to trigger a payment event
Receive Webhook
Your registered webhook endpoint receives the transaction status
Complete Example with Error Handling
Here’s a production-ready example with proper error handling:
require ( 'dotenv' ). config ();
const axios = require ( 'axios' );
const jose = require ( 'jose' );
const fs = require ( 'fs' );
class QRPaymentService {
constructor ( config ) {
this . secretKey = config . secretKey ;
this . encryptionKey = Buffer . from ( config . encryptionKey , 'utf-8' );
this . activationCode = config . activationCode ;
this . baseURL = config . baseURL || 'https://qrph.sbx.moduluslabs.io' ;
}
async encrypt ( data ) {
const jwe = await new jose . CompactEncrypt (
new TextEncoder (). encode ( JSON . stringify ( data ))
)
. setProtectedHeader ({ alg: 'A256KW' , enc: 'A256CBC-HS512' })
. encrypt ( this . encryptionKey );
return jwe ;
}
async decrypt ( jweToken ) {
const { plaintext } = await jose . compactDecrypt ( jweToken , this . encryptionKey );
return JSON . parse ( new TextDecoder (). decode ( plaintext ));
}
validateAmount ( amount ) {
const num = parseFloat ( amount );
if ( isNaN ( num ) || num < 1.00 || num > 99999.99 ) {
throw new Error ( 'Amount must be between 1.00 and 99999.99' );
}
return num . toFixed ( 2 );
}
async createQR ( amount , merchantReferenceNumber ) {
try {
// Validate inputs
const validAmount = this . validateAmount ( amount );
if ( ! merchantReferenceNumber || merchantReferenceNumber . length > 36 ) {
throw new Error ( 'Invalid merchant reference number' );
}
// Prepare and encrypt payload
const payload = {
activationCode: this . activationCode ,
currency: 'PHP' ,
amount: validAmount ,
merchantReferenceNumber
};
const jweToken = await this . encrypt ( payload );
// Make API request
const response = await axios . post (
` ${ this . baseURL } /v1/pay/qr` ,
{ Token: jweToken },
{
auth: { username: this . secretKey , password: '' },
headers: { 'Content-Type' : 'application/json' },
timeout: 10000 // 10 second timeout
}
);
// Decrypt and return result
const result = await this . decrypt ( response . data . Token );
return {
success: true ,
data: result
};
} catch ( error ) {
// Handle errors
if ( error . response ) {
const { status , data } = error . response ;
// Try to decrypt error if it's a JWE token
let errorDetails = data ;
if ( data . Token ) {
try {
errorDetails = await this . decrypt ( data . Token );
} catch ( decryptError ) {
console . error ( 'Could not decrypt error response' );
}
}
return {
success: false ,
error: {
status ,
code: errorDetails . code ,
message: errorDetails . error ,
referenceNumber: errorDetails . referenceNumber
}
};
}
return {
success: false ,
error: {
message: error . message
}
};
}
}
saveQRCode ( qrBody , filename ) {
const buffer = Buffer . from ( qrBody , 'base64' );
fs . writeFileSync ( filename , buffer );
return filename ;
}
}
// Usage
async function main () {
const service = new QRPaymentService ({
secretKey: process . env . MODULUS_SECRET_KEY ,
encryptionKey: process . env . MODULUS_ENCRYPTION_KEY ,
activationCode: process . env . MODULUS_ACTIVATION_CODE
});
const result = await service . createQR ( 500.00 , `ORDER- ${ Date . now () } ` );
if ( result . success ) {
console . log ( ' QR Code created successfully' );
console . log ( 'Transaction ID:' , result . data . id );
const filename = service . saveQRCode (
result . data . qrBody ,
`qr- ${ result . data . id } .png`
);
console . log ( 'Saved to:' , filename );
} else {
console . error ( 'Failed to create QR code' );
console . error ( 'Error:' , result . error );
}
}
main ();
What’s Next?
Now that you’ve created your first QR code, here are some next steps:
Common Issues
Solution:
Check your .env file has the correct MODULUS_SECRET_KEY
Ensure no extra spaces or quotes around the key
Verify you’re using sandbox credentials for sandbox URL
Error: Cannot decrypt token
Solution:
Verify your MODULUS_ENCRYPTION_KEY is correct
Ensure the key hasn’t been modified (no spaces, newlines)
Check you’re using UTF-8 encoding for the key
Error: Amount validation failed
Solution:
Ensure amount is between 1.00 and 99999.99
Pass amount as a string with 2 decimal places: "500.00"
Use .toFixed(2) to format numbers correctly
QR code file is corrupted
Solution:
Ensure you’re decoding the full Base64 string
Check the file was written in binary mode
Verify no encoding issues during Base64 decode
Need Help?
You’re all set! You’ve successfully created your first Dynamic QR Ph payment. The QR code is ready to be scanned by customers using their banking apps.