Skip to main content

Overview

The Modulus Labs Sandbox environment allows you to test your QR Ph integration without processing real payments. The sandbox mirrors production behavior, making it perfect for development and testing.
Sandbox vs. Production: The sandbox environment uses separate credentials and URLs. No real money changes hands in sandbox mode.

Prerequisites

Before testing, ensure you have:
1

Sandbox Credentials

  • Secret Key (for authentication)
  • Encryption Key (for JWE tokens)
  • Activation Code (for your sub-merchant account)
2

Webhook Integration

Register a webhook endpoint using the Modulus Labs Webhooks API to receive payment notifications
3

Development Environment

Set up your local development environment with the required libraries (jose, axios, etc.)

Testing Workflow

Testing Dynamic QR Ph requires simulating the entire payment flow:
1

Register Webhook

Create a webhook endpoint to receive transaction status updates
2

Create QR Code

Generate a Dynamic QR Ph using the Create API
3

Convert QR Body

Convert the Base64-encoded QR body to an image
4

Decode QR Image

Extract the raw QR value from the generated image
5

Simulate Payment

Use the Simulate Webhook API to trigger a payment event
6

Receive Webhook

Your webhook endpoint receives the transaction status

Step 1: Register a Webhook

First, register your webhook URL to receive payment notifications:
curl -X POST https://qrph.sbx.moduluslabs.io/v1/webhooks \
  -u sk_YOUR_SECRET_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://your-app.com/webhooks/qr-payment",
    "events": ["payment.succeeded", "payment.failed"]
  }'
Use services like webhook.site or ngrok to create temporary webhook endpoints for testing.

Step 2: Create a Dynamic QR Code

Generate a QR code for testing:
const axios = require('axios');
const jose = require('jose');

const SECRET_KEY = 'sk_YOUR_SECRET_KEY';
const ENCRYPTION_KEY = 'YOUR_ENCRYPTION_KEY';

async function createQR() {
  // Encrypt payload
  const payload = {
    activationCode: 'A9X4-B7P2-Q6Z8-M3L5',
    currency: 'PHP',
    amount: '500.00',
    merchantReferenceNumber: crypto.randomUUID()
  };

  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);

  // Make API request
  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' }
    }
  );

  // Decrypt response
  const { plaintext } = await jose.compactDecrypt(response.data.Token, key);
  const result = JSON.parse(new TextDecoder().decode(plaintext));

  console.log('Transaction ID:', result.id);
  console.log('QR Body (Base64):', result.qrBody);

  return result;
}

createQR();
Response (encrypted JWE token):
{
  "Token": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..."
}
Decrypted payload:
{
  "id": "0ce32626-08cf-405c-adab-6d384a8871da",
  "qrBody": "iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAMTElEQVR4nO..."
}

Step 3: Convert Base64 to QR Image

The qrBody field contains a Base64-encoded PNG image. Convert it to an image file:
const fs = require('fs');

function saveQRImage(base64Data, filename = 'qr-code.png') {
  const buffer = Buffer.from(base64Data, 'base64');
  fs.writeFileSync(filename, buffer);
  console.log(`QR code saved to ${filename}`);
}

// Usage
const result = await createQR();
saveQRImage(result.qrBody, 'test-qr.png');

Online Converter (For Quick Testing)

Alternatively, use this online tool:
  1. Visit https://base64.guru/converter/decode/image/png
  2. Paste your Base64-encoded QR body
  3. Click “Decode” to generate the image
  4. Download the PNG file
Example QR Code:
Example QR Ph Code

Step 4: Decode the QR Image

Extract the raw QR value from the image:

Using Online Tool

  1. Visit https://zxing.org/w/decode.jspx
  2. Upload your QR code image
  3. Click “Submit”
  4. Copy the raw text value
Example raw QR value:
00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000905033015204601653036085407 1500.005802PH5912MAIN ACCOUNT6006MANILA63041943

Using Code

const fs = require('fs');
const { PNG } = require('pngjs');
const jsQR = require('jsqr');

function decodeQR(filename) {
  const data = fs.readFileSync(filename);
  const png = PNG.sync.read(data);

  const code = jsQR(
    Uint8ClampedArray.from(png.data),
    png.width,
    png.height
  );

  if (code) {
    console.log('QR Value:', code.data);
    return code.data;
  } else {
    throw new Error('Could not decode QR code');
  }
}

// Usage
const qrValue = decodeQR('test-qr.png');

Step 5: Simulate Payment

Now simulate a payment by calling the Simulate Webhook API:
const axios = require('axios');

async function simulatePayment(qrValue, useCase = 'SUCCESS') {
  const response = await axios.post(
    'https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate',
    {
      qrBody: qrValue,
      useCase: useCase  // 'SUCCESS' or error code
    },
    {
      auth: { username: SECRET_KEY, password: '' },
      headers: { 'Content-Type': 'application/json' }
    }
  );

  console.log('Simulation triggered:', response.data);
  return response.data;
}

// Usage - Successful payment
await simulatePayment(qrValue, 'SUCCESS');

// Usage - Failed payment
await simulatePayment(qrValue, 'MISSING_PARTNER_REVENUE_ACCOUNT');

Step 6: Receive Webhook Notification

After simulating the payment, your webhook endpoint will receive a notification:

Successful Payment Webhook

{
  "event": "payment.succeeded",
  "transactionId": "0ce32626-08cf-405c-adab-6d384a8871da",
  "merchantReferenceNumber": "5e91bc6c-f7e2-4a39-ae0e-5e93985c94a4",
  "amount": "500.00",
  "currency": "PHP",
  "status": "succeeded",
  "completedAt": "2025-01-29T10:30:00Z"
}

Failed Payment Webhook

{
  "event": "payment.failed",
  "transactionId": "0ce32626-08cf-405c-adab-6d384a8871da",
  "merchantReferenceNumber": "5e91bc6c-f7e2-4a39-ae0e-5e93985c94a4",
  "amount": "500.00",
  "currency": "PHP",
  "status": "failed",
  "errorCode": "MISSING_PARTNER_REVENUE_ACCOUNT",
  "errorMessage": "Partner revenue account not found",
  "failedAt": "2025-01-29T10:30:00Z"
}

Test Scenarios

Test different scenarios to ensure your integration handles all cases:
Use Case: SUCCESSExpected Result: Webhook receives payment.succeeded eventTest: Verify your app processes successful payments correctly
Use Case: MISSING_PARTNER_REVENUE_ACCOUNTExpected Result: Webhook receives payment.failed eventTest: Verify your app handles this specific error gracefully
Use Case: INSUFFICIENT_FUNDSExpected Result: Webhook receives payment.failed eventTest: Ensure user is informed of insufficient funds
Use Case: Use a malformed or expired QR valueExpected Result: API returns error before webhook is sentTest: Verify error handling for invalid QR codes
Use Case: Reuse the same merchantReferenceNumberExpected Result: API rejects duplicate transactionTest: Ensure idempotency is working correctly
Use Case: Delay webhook simulation by 30+ secondsExpected Result: Test timeout handling in your applicationTest: Verify your app doesn’t hang waiting for webhook

Complete Test Script

Here’s a complete end-to-end test script:
const axios = require('axios');
const jose = require('jose');
const fs = require('fs');

const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
const ENCRYPTION_KEY = process.env.MODULUS_ENCRYPTION_KEY;

async function runFullTest() {
  console.log('🧪 Starting QR Ph Integration Test\n');

  try {
    // Step 1: Create QR Code
    console.log('📝 Step 1: Creating Dynamic QR Code...');
    const payload = {
      activationCode: 'A9X4-B7P2-Q6Z8-M3L5',
      currency: 'PHP',
      amount: '500.00',
      merchantReferenceNumber: `TEST-${Date.now()}`
    };

    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);

    const createResponse = await axios.post(
      'https://qrph.sbx.moduluslabs.io/v1/pay/qr',
      { Token: jweToken },
      {
        auth: { username: SECRET_KEY, password: '' },
        headers: { 'Content-Type': 'application/json' }
      }
    );

    const { plaintext } = await jose.compactDecrypt(createResponse.data.Token, key);
    const qrData = JSON.parse(new TextDecoder().decode(plaintext));
    console.log(` QR Created - ID: ${qrData.id}\n`);

    // Step 2: Save QR Image
    console.log(' Step 2: Saving QR Image...');
    const buffer = Buffer.from(qrData.qrBody, 'base64');
    fs.writeFileSync('test-qr.png', buffer);
    console.log(' QR saved to test-qr.png\n');

    // Step 3: Decode QR (manual step - using placeholder)
    console.log(' Step 3: Decode QR manually using:');
    console.log('   https://zxing.org/w/decode.jspx');
    console.log('   Upload: test-qr.png\n');

    // Step 4: Simulate Payment (after you get qrValue from decoding)
    console.log(' Step 4: Ready to simulate payment');
    console.log('   Run: simulatePayment(qrValue, "SUCCESS")');
    console.log('\n Test setup complete!');

    return qrData;

  } catch (error) {
    console.error(' Test failed:', error.message);
    if (error.response) {
      console.error('Response:', error.response.data);
    }
    throw error;
  }
}

// Run the test
runFullTest();

Debugging Tips

Enable Verbose Logging

Log all requests, responses, and encryption/decryption steps

Use Webhook.site

Test webhook delivery without setting up a server

Check Base64 Encoding

Ensure Base64 strings aren’t corrupted during copy/paste

Verify Credentials

Double-check you’re using sandbox credentials, not production

Test Incrementally

Test each step independently before running end-to-end

Monitor Webhook Logs

Check webhook server logs for incoming requests

Common Issues

  • Ensure the image was saved correctly from Base64
  • Try a different QR decoder tool
  • Check if the image file is corrupted
  • Verify the Base64 string wasn’t truncated
  • Verify webhook URL is publicly accessible
  • Check webhook registration was successful
  • Ensure firewall isn’t blocking incoming requests
  • Use webhook.site for initial testing
  • Verify the QR value was decoded correctly
  • Check that the transaction hasn’t been used already
  • Ensure proper authentication
  • Verify the useCase parameter is valid

Next Steps