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:
Sandbox Credentials
Secret Key (for authentication)
Encryption Key (for JWE tokens)
Activation Code (for your sub-merchant account)
Webhook Integration
Register a webhook endpoint using the Modulus Labs Webhooks API to receive payment notifications
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:
Register Webhook
Create a webhook endpoint to receive transaction status updates
Create QR Code
Generate a Dynamic QR Ph using the Create API
Convert QR Body
Convert the Base64-encoded QR body to an image
Decode QR Image
Extract the raw QR value from the generated image
Simulate Payment
Use the Simulate Webhook API to trigger a payment event
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:
Node.js
cURL
Python
PHP
Go
Java
C# / .NET
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 , 'base64' );
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:
Node.js
Python
Command Line
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:
Visit https://base64.guru/converter/decode/image/png
Paste your Base64-encoded QR body
Click “Decode” to generate the image
Download the PNG file
Example QR Code:
Step 4: Decode the QR Image
Extract the raw QR value from the image:
Visit https://zxing.org/w/decode.jspx
Upload your QR code image
Click “Submit”
Copy the raw text value
Example raw QR value:
00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000905033015204601653036085407 1500.005802PH5912MAIN ACCOUNT6006MANILA63041943
Using Code
Node.js (using jsqr)
Python (using pyzbar)
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:
Node.js
cURL
Python
PHP
Go
Java
C# / .NET
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 , 'base64' );
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