Skip to main content

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

1

Get Your Credentials

Contact Modulus Labs to receive:
  • Secret Key (starts with sk_)
  • Encryption Key
  • Activation Code (format: XXXX-XXXX-XXXX-XXXX)
2

Install Node.js

Ensure you have Node.js 14+ installed
node --version
# v18.0.0 or higher
3

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:
.env
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:
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:
node create-qr.js
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:

1

Decode QR Code

Upload your QR image to zxing.org/w/decode.jspx to get the raw QR value
2

Simulate Payment

Use the raw QR value with the Simulate Webhook API to trigger a payment event
3

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:
production-example.js
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
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
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
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.