# Note: You need to encrypt the payload first using a script/tool
# See the Encryption Guide for JWE token creation
# 1. Create encrypted JWE token containing the simulate payload
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."
# 2. Make API request
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
-u sk_your_secret_key: \
-H "Content-Type: application/json" \
-d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
# 1. Create encrypted JWE token containing: {"qrBody": "...", "useCase": "MISSING_DESTINATION_ACCOUNT"}
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."
# 2. Make API request
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
-u sk_your_secret_key: \
-H "Content-Type: application/json" \
-d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
const axios = require('axios');
const jose = require('jose');
const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
const ENCRYPTION_KEY = process.env.MODULUS_ENCRYPTION_KEY;
async function simulateWebhook(qrBody, useCase) {
// 1. Prepare the payload
const payload = { qrBody, useCase };
// 2. Encrypt payload into JWE token
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);
// 3. Make API request
const response = await axios.post(
'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
{ request: { Token: jweToken } },
{
auth: { username: SECRET_KEY, password: '' },
headers: { 'Content-Type': 'application/json' }
}
);
console.log('Webhook simulation sent');
console.log('Response:', response.data);
return response.data;
}
const qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';
// Test successful payment
simulateWebhook(qrBody, 'SUCCESS')
.catch(error => {
console.error('Error:', error.response?.data || error.message);
});
// Test declined payment
simulateWebhook(qrBody, 'MISSING_DESTINATION_ACCOUNT');
import os
import json
import requests
from jose import jwe
SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
ENCRYPTION_KEY = os.getenv('MODULUS_ENCRYPTION_KEY')
def simulate_webhook(qr_body, use_case):
# 1. Prepare the payload
payload = {
'qrBody': qr_body,
'useCase': use_case
}
# 2. Encrypt payload into JWE token
jwe_token = jwe.encrypt(
json.dumps(payload),
ENCRYPTION_KEY,
algorithm='A256KW',
encryption='A256CBC-HS512'
)
# 3. Make API request
response = requests.post(
'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
json={'request': {'Token': jwe_token}},
auth=(SECRET_KEY, ''),
headers={'Content-Type': 'application/json'}
)
response.raise_for_status()
result = response.json()
print('Webhook simulation sent')
print(f"Response: {result}")
return result
qr_body = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0'
# Test successful payment
simulate_webhook(qr_body, 'SUCCESS')
# Test declined payment
simulate_webhook(qr_body, 'MISSING_DESTINATION_ACCOUNT')
<?php
require 'vendor/autoload.php';
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\Serializer\CompactSerializer;
$secretKey = getenv('MODULUS_SECRET_KEY');
$encryptionKey = getenv('MODULUS_ENCRYPTION_KEY');
function simulateWebhook($qrBody, $useCase) {
global $secretKey, $encryptionKey;
// 1. Prepare the payload
$payload = [
'qrBody' => $qrBody,
'useCase' => $useCase
];
// 2. Encrypt payload (simplified - use proper JWE library)
// See encryption documentation for full implementation
$jweToken = encryptToJWE(json_encode($payload), $encryptionKey);
// 3. Make API request
$ch = curl_init('https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['request' => ['Token' => $jweToken]]));
curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$result = json_decode($response, true);
echo "Webhook simulation sent\n";
echo "Response: " . json_encode($result) . "\n";
return $result;
} else {
echo "Error: HTTP $httpCode\n";
echo "Details: $response\n";
return null;
}
}
$qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';
// Test successful payment
simulateWebhook($qrBody, 'SUCCESS');
// Test declined payment
simulateWebhook($qrBody, 'MISSING_DESTINATION_ACCOUNT');
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
)
type SimulatePayload struct {
QRBody string `json:"qrBody"`
UseCase string `json:"useCase"`
}
type TokenWrapper struct {
Token string `json:"Token"`
}
type TokenRequest struct {
Request TokenWrapper `json:"request"`
}
func simulateWebhook(qrBody, useCase string) error {
secretKey := os.Getenv("MODULUS_SECRET_KEY")
encryptionKey := os.Getenv("MODULUS_ENCRYPTION_KEY")
// 1. Prepare the payload
payload := SimulatePayload{
QRBody: qrBody,
UseCase: useCase,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
// 2. Encrypt payload into JWE token
keyBytes, err := base64.RawURLEncoding.DecodeString(encryptionKey)
if err != nil {
return fmt.Errorf("failed to decode encryption key: %w", err)
}
encrypted, err := jwe.Encrypt(payloadJSON,
jwe.WithKey(jwa.A256KW, keyBytes),
jwe.WithContentEncryption(jwa.A256CBC_HS512),
)
if err != nil {
return fmt.Errorf("failed to encrypt payload: %w", err)
}
jweToken := string(encrypted)
// 3. Make API request with Basic Auth
reqBody := TokenRequest{Request: TokenWrapper{Token: jweToken}}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.SetBasicAuth(secretKey, "")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 200 {
fmt.Printf("Webhook simulation sent (%s)\n", useCase)
fmt.Printf("Response: %s\n", string(body))
return nil
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
func main() {
qrBody := "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0"
// Test successful payment
if err := simulateWebhook(qrBody, "SUCCESS"); err != nil {
fmt.Printf("Error: %v\n", err)
}
// Test declined payment
if err := simulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT"); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Jose;
namespace ModulusWebhooks;
class Program
{
static async Task Main(string[] args)
{
var qrBody = "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0";
// Test successful payment simulation
await SimulateWebhook(qrBody, "SUCCESS");
// Test declined payment simulation
await SimulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT");
}
static byte[] Base64UrlDecode(string base64Url)
{
var base64 = base64Url
.Replace('-', '+')
.Replace('_', '/');
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
static async Task SimulateWebhook(string qrBody, string useCase)
{
var secretKey = Environment.GetEnvironmentVariable("MODULUS_SECRET_KEY");
var encryptionKey = Environment.GetEnvironmentVariable("MODULUS_ENCRYPTION_KEY");
if (string.IsNullOrEmpty(secretKey) || string.IsNullOrEmpty(encryptionKey))
{
Console.WriteLine("Error: MODULUS_SECRET_KEY and MODULUS_ENCRYPTION_KEY must be set");
return;
}
try
{
// 1. Prepare and encrypt the payload
var payload = new { qrBody, useCase };
var key = Base64UrlDecode(encryptionKey);
var payloadJson = JsonSerializer.Serialize(payload);
var jweToken = JWT.Encode(payloadJson, key, JweAlgorithm.A256KW, JweEncryption.A256CBC_HS512);
// 2. Make API request with Basic Auth
using var client = new HttpClient();
var authBytes = Encoding.ASCII.GetBytes($"{secretKey}:");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));
var requestBody = new { request = new { Token = jweToken } };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
Encoding.UTF8,
"application/json"
);
var response = await client.PostAsync(
"https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate",
content
);
var responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"{useCase} webhook simulation sent successfully");
Console.WriteLine($"Response: {responseBody}");
}
else
{
Console.WriteLine($"Error: HTTP {(int)response.StatusCode}");
Console.WriteLine($"Details: {responseBody}");
}
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
}
}
{
"id": "a78efd32-de3b-4854-b599-11ae9f98f97e"
}
{
"code": "20000003",
"error": "useCase is invalid.",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "10000013",
"error": "Invalid API Key",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "20000005",
"error": "No enabled webhooks found",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "10000001",
"error": "An unexpected error occurred. Please try again later.",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
Simulate Webhook
Test your webhook integration by triggering simulated transaction notifications in sandbox
# Note: You need to encrypt the payload first using a script/tool
# See the Encryption Guide for JWE token creation
# 1. Create encrypted JWE token containing the simulate payload
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."
# 2. Make API request
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
-u sk_your_secret_key: \
-H "Content-Type: application/json" \
-d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
# 1. Create encrypted JWE token containing: {"qrBody": "...", "useCase": "MISSING_DESTINATION_ACCOUNT"}
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."
# 2. Make API request
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
-u sk_your_secret_key: \
-H "Content-Type: application/json" \
-d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
const axios = require('axios');
const jose = require('jose');
const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
const ENCRYPTION_KEY = process.env.MODULUS_ENCRYPTION_KEY;
async function simulateWebhook(qrBody, useCase) {
// 1. Prepare the payload
const payload = { qrBody, useCase };
// 2. Encrypt payload into JWE token
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);
// 3. Make API request
const response = await axios.post(
'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
{ request: { Token: jweToken } },
{
auth: { username: SECRET_KEY, password: '' },
headers: { 'Content-Type': 'application/json' }
}
);
console.log('Webhook simulation sent');
console.log('Response:', response.data);
return response.data;
}
const qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';
// Test successful payment
simulateWebhook(qrBody, 'SUCCESS')
.catch(error => {
console.error('Error:', error.response?.data || error.message);
});
// Test declined payment
simulateWebhook(qrBody, 'MISSING_DESTINATION_ACCOUNT');
import os
import json
import requests
from jose import jwe
SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
ENCRYPTION_KEY = os.getenv('MODULUS_ENCRYPTION_KEY')
def simulate_webhook(qr_body, use_case):
# 1. Prepare the payload
payload = {
'qrBody': qr_body,
'useCase': use_case
}
# 2. Encrypt payload into JWE token
jwe_token = jwe.encrypt(
json.dumps(payload),
ENCRYPTION_KEY,
algorithm='A256KW',
encryption='A256CBC-HS512'
)
# 3. Make API request
response = requests.post(
'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
json={'request': {'Token': jwe_token}},
auth=(SECRET_KEY, ''),
headers={'Content-Type': 'application/json'}
)
response.raise_for_status()
result = response.json()
print('Webhook simulation sent')
print(f"Response: {result}")
return result
qr_body = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0'
# Test successful payment
simulate_webhook(qr_body, 'SUCCESS')
# Test declined payment
simulate_webhook(qr_body, 'MISSING_DESTINATION_ACCOUNT')
<?php
require 'vendor/autoload.php';
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\Serializer\CompactSerializer;
$secretKey = getenv('MODULUS_SECRET_KEY');
$encryptionKey = getenv('MODULUS_ENCRYPTION_KEY');
function simulateWebhook($qrBody, $useCase) {
global $secretKey, $encryptionKey;
// 1. Prepare the payload
$payload = [
'qrBody' => $qrBody,
'useCase' => $useCase
];
// 2. Encrypt payload (simplified - use proper JWE library)
// See encryption documentation for full implementation
$jweToken = encryptToJWE(json_encode($payload), $encryptionKey);
// 3. Make API request
$ch = curl_init('https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['request' => ['Token' => $jweToken]]));
curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$result = json_decode($response, true);
echo "Webhook simulation sent\n";
echo "Response: " . json_encode($result) . "\n";
return $result;
} else {
echo "Error: HTTP $httpCode\n";
echo "Details: $response\n";
return null;
}
}
$qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';
// Test successful payment
simulateWebhook($qrBody, 'SUCCESS');
// Test declined payment
simulateWebhook($qrBody, 'MISSING_DESTINATION_ACCOUNT');
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
)
type SimulatePayload struct {
QRBody string `json:"qrBody"`
UseCase string `json:"useCase"`
}
type TokenWrapper struct {
Token string `json:"Token"`
}
type TokenRequest struct {
Request TokenWrapper `json:"request"`
}
func simulateWebhook(qrBody, useCase string) error {
secretKey := os.Getenv("MODULUS_SECRET_KEY")
encryptionKey := os.Getenv("MODULUS_ENCRYPTION_KEY")
// 1. Prepare the payload
payload := SimulatePayload{
QRBody: qrBody,
UseCase: useCase,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
// 2. Encrypt payload into JWE token
keyBytes, err := base64.RawURLEncoding.DecodeString(encryptionKey)
if err != nil {
return fmt.Errorf("failed to decode encryption key: %w", err)
}
encrypted, err := jwe.Encrypt(payloadJSON,
jwe.WithKey(jwa.A256KW, keyBytes),
jwe.WithContentEncryption(jwa.A256CBC_HS512),
)
if err != nil {
return fmt.Errorf("failed to encrypt payload: %w", err)
}
jweToken := string(encrypted)
// 3. Make API request with Basic Auth
reqBody := TokenRequest{Request: TokenWrapper{Token: jweToken}}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.SetBasicAuth(secretKey, "")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 200 {
fmt.Printf("Webhook simulation sent (%s)\n", useCase)
fmt.Printf("Response: %s\n", string(body))
return nil
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
func main() {
qrBody := "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0"
// Test successful payment
if err := simulateWebhook(qrBody, "SUCCESS"); err != nil {
fmt.Printf("Error: %v\n", err)
}
// Test declined payment
if err := simulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT"); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Jose;
namespace ModulusWebhooks;
class Program
{
static async Task Main(string[] args)
{
var qrBody = "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0";
// Test successful payment simulation
await SimulateWebhook(qrBody, "SUCCESS");
// Test declined payment simulation
await SimulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT");
}
static byte[] Base64UrlDecode(string base64Url)
{
var base64 = base64Url
.Replace('-', '+')
.Replace('_', '/');
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
static async Task SimulateWebhook(string qrBody, string useCase)
{
var secretKey = Environment.GetEnvironmentVariable("MODULUS_SECRET_KEY");
var encryptionKey = Environment.GetEnvironmentVariable("MODULUS_ENCRYPTION_KEY");
if (string.IsNullOrEmpty(secretKey) || string.IsNullOrEmpty(encryptionKey))
{
Console.WriteLine("Error: MODULUS_SECRET_KEY and MODULUS_ENCRYPTION_KEY must be set");
return;
}
try
{
// 1. Prepare and encrypt the payload
var payload = new { qrBody, useCase };
var key = Base64UrlDecode(encryptionKey);
var payloadJson = JsonSerializer.Serialize(payload);
var jweToken = JWT.Encode(payloadJson, key, JweAlgorithm.A256KW, JweEncryption.A256CBC_HS512);
// 2. Make API request with Basic Auth
using var client = new HttpClient();
var authBytes = Encoding.ASCII.GetBytes($"{secretKey}:");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));
var requestBody = new { request = new { Token = jweToken } };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
Encoding.UTF8,
"application/json"
);
var response = await client.PostAsync(
"https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate",
content
);
var responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"{useCase} webhook simulation sent successfully");
Console.WriteLine($"Response: {responseBody}");
}
else
{
Console.WriteLine($"Error: HTTP {(int)response.StatusCode}");
Console.WriteLine($"Details: {responseBody}");
}
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
}
}
{
"id": "a78efd32-de3b-4854-b599-11ae9f98f97e"
}
{
"code": "20000003",
"error": "useCase is invalid.",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "10000013",
"error": "Invalid API Key",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "20000005",
"error": "No enabled webhooks found",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "10000001",
"error": "An unexpected error occurred. Please try again later.",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
Overview
The Simulate Webhook endpoint allows you to test your webhook integration without creating real QR codes or processing actual transactions. It sends encrypted webhook notifications to your registered endpoint, exactly like real transaction webhooks.Endpoint
POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate
Authentication
This endpoint requires HTTP Basic Authentication using your Secret Key.Authorization: Basic {base64(secret_key:)}
Request
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Basic {base64(secret_key:)} | Yes |
Content-Type | application/json | Yes |
Body Parameters (Encrypted Payload)
Token field. See the Encryption Guide for details on creating JWE tokens.| Field | Type | Required | Description |
|---|---|---|---|
qrBody | string | Yes | Raw QRPH payload used to simulate a transaction |
useCase | string | Yes | Transaction event type: SUCCESS, MISSING_DESTINATION_ACCOUNT, MISSING_PARTNER_REVENUE_ACCOUNT, or UNSUPPORTED_TRANSFER_TYPE |
qrBody
qrBody
- Example:
"00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0"
useCase
useCase
SUCCESS- Simulates a successful paymentMISSING_DESTINATION_ACCOUNT- Simulates a declined payment (missing destination account)MISSING_PARTNER_REVENUE_ACCOUNT- Simulates a declined payment (missing partner revenue account)UNSUPPORTED_TRANSFER_TYPE- Simulates a declined payment (unsupported transfer type)
"SUCCESS"Example Payloads (Before Encryption)
{
"qrBody":
"00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0",
"useCase": "SUCCESS"
}
{
"qrBody":
"00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0",
"useCase": "MISSING_DESTINATION_ACCOUNT"
}
Response
Success Response
Status Code:200 OK
Returns confirmation that the simulated webhook was sent to your registered endpoint.
id returned from the Create Dynamic QR Ph response).Example: "a78efd32-de3b-4854-b599-11ae9f98f97e"Response Example
{
"id": "a78efd32-de3b-4854-b599-11ae9f98f97e"
}
{
"id": "a78efd32-de3b-4854-b599-11ae9f98f97e"
}
{
"code": "20000003",
"error": "useCase is invalid.",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "10000013",
"error": "Invalid API Key",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "20000005",
"error": "No enabled webhooks found",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
{
"code": "10000001",
"error": "An unexpected error occurred. Please try again later.",
"referenceNumber": "097bf60a-bdab-40bf-b615-3c0eae693b86"
}
Error Responses
400 Bad Request
400 Bad Request
400Causes:- Missing
useCasefield - Invalid
useCasevalue
{
"code": "20000003",
"error": "useCase is invalid.",
"referenceNumber": "abc123-def456-ghi789"
}
- Ensure
useCaseis one of:SUCCESS,MISSING_DESTINATION_ACCOUNT,MISSING_PARTNER_REVENUE_ACCOUNT, orUNSUPPORTED_TRANSFER_TYPE - Check spelling and capitalization
401 Unauthorized
401 Unauthorized
401Cause: Invalid or missing authentication credentialsSolution:- Verify your secret key is correct
- Ensure Authorization header format:
Basic {base64(secret_key:)}
404 Not Found
404 Not Found
404Cause: No enabled webhooks configuredResponse Example:{
"code": "20000005",
"error": "No enabled webhooks found",
"referenceNumber": "abc123-def456-ghi789"
}
- Create at least one webhook using Create Webhook API
- Ensure webhook status is
ENABLED - Verify webhook actions include the simulated event type
500 Internal Server Error
500 Internal Server Error
500Cause: Unexpected server errorSolution:- Retry the request
- If the issue persists, contact Modulus Labs support
# Note: You need to encrypt the payload first using a script/tool
# See the Encryption Guide for JWE token creation
# 1. Create encrypted JWE token containing the simulate payload
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."
# 2. Make API request
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
-u sk_your_secret_key: \
-H "Content-Type: application/json" \
-d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
# 1. Create encrypted JWE token containing: {"qrBody": "...", "useCase": "MISSING_DESTINATION_ACCOUNT"}
ENCRYPTED_TOKEN="eyJhbGciOiJBMjU2S1ciL..."
# 2. Make API request
curl -X POST https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate \
-u sk_your_secret_key: \
-H "Content-Type: application/json" \
-d "{\"request\": {\"Token\": \"$ENCRYPTED_TOKEN\"}}"
const axios = require('axios');
const jose = require('jose');
const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
const ENCRYPTION_KEY = process.env.MODULUS_ENCRYPTION_KEY;
async function simulateWebhook(qrBody, useCase) {
// 1. Prepare the payload
const payload = { qrBody, useCase };
// 2. Encrypt payload into JWE token
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);
// 3. Make API request
const response = await axios.post(
'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
{ request: { Token: jweToken } },
{
auth: { username: SECRET_KEY, password: '' },
headers: { 'Content-Type': 'application/json' }
}
);
console.log('Webhook simulation sent');
console.log('Response:', response.data);
return response.data;
}
const qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';
// Test successful payment
simulateWebhook(qrBody, 'SUCCESS')
.catch(error => {
console.error('Error:', error.response?.data || error.message);
});
// Test declined payment
simulateWebhook(qrBody, 'MISSING_DESTINATION_ACCOUNT');
import os
import json
import requests
from jose import jwe
SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
ENCRYPTION_KEY = os.getenv('MODULUS_ENCRYPTION_KEY')
def simulate_webhook(qr_body, use_case):
# 1. Prepare the payload
payload = {
'qrBody': qr_body,
'useCase': use_case
}
# 2. Encrypt payload into JWE token
jwe_token = jwe.encrypt(
json.dumps(payload),
ENCRYPTION_KEY,
algorithm='A256KW',
encryption='A256CBC-HS512'
)
# 3. Make API request
response = requests.post(
'https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate',
json={'request': {'Token': jwe_token}},
auth=(SECRET_KEY, ''),
headers={'Content-Type': 'application/json'}
)
response.raise_for_status()
result = response.json()
print('Webhook simulation sent')
print(f"Response: {result}")
return result
qr_body = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0'
# Test successful payment
simulate_webhook(qr_body, 'SUCCESS')
# Test declined payment
simulate_webhook(qr_body, 'MISSING_DESTINATION_ACCOUNT')
<?php
require 'vendor/autoload.php';
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\Serializer\CompactSerializer;
$secretKey = getenv('MODULUS_SECRET_KEY');
$encryptionKey = getenv('MODULUS_ENCRYPTION_KEY');
function simulateWebhook($qrBody, $useCase) {
global $secretKey, $encryptionKey;
// 1. Prepare the payload
$payload = [
'qrBody' => $qrBody,
'useCase' => $useCase
];
// 2. Encrypt payload (simplified - use proper JWE library)
// See encryption documentation for full implementation
$jweToken = encryptToJWE(json_encode($payload), $encryptionKey);
// 3. Make API request
$ch = curl_init('https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['request' => ['Token' => $jweToken]]));
curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$result = json_decode($response, true);
echo "Webhook simulation sent\n";
echo "Response: " . json_encode($result) . "\n";
return $result;
} else {
echo "Error: HTTP $httpCode\n";
echo "Details: $response\n";
return null;
}
}
$qrBody = '00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0';
// Test successful payment
simulateWebhook($qrBody, 'SUCCESS');
// Test declined payment
simulateWebhook($qrBody, 'MISSING_DESTINATION_ACCOUNT');
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
)
type SimulatePayload struct {
QRBody string `json:"qrBody"`
UseCase string `json:"useCase"`
}
type TokenWrapper struct {
Token string `json:"Token"`
}
type TokenRequest struct {
Request TokenWrapper `json:"request"`
}
func simulateWebhook(qrBody, useCase string) error {
secretKey := os.Getenv("MODULUS_SECRET_KEY")
encryptionKey := os.Getenv("MODULUS_ENCRYPTION_KEY")
// 1. Prepare the payload
payload := SimulatePayload{
QRBody: qrBody,
UseCase: useCase,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal payload: %w", err)
}
// 2. Encrypt payload into JWE token
keyBytes, err := base64.RawURLEncoding.DecodeString(encryptionKey)
if err != nil {
return fmt.Errorf("failed to decode encryption key: %w", err)
}
encrypted, err := jwe.Encrypt(payloadJSON,
jwe.WithKey(jwa.A256KW, keyBytes),
jwe.WithContentEncryption(jwa.A256CBC_HS512),
)
if err != nil {
return fmt.Errorf("failed to encrypt payload: %w", err)
}
jweToken := string(encrypted)
// 3. Make API request with Basic Auth
reqBody := TokenRequest{Request: TokenWrapper{Token: jweToken}}
jsonData, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.SetBasicAuth(secretKey, "")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode == 200 {
fmt.Printf("Webhook simulation sent (%s)\n", useCase)
fmt.Printf("Response: %s\n", string(body))
return nil
}
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
func main() {
qrBody := "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0"
// Test successful payment
if err := simulateWebhook(qrBody, "SUCCESS"); err != nil {
fmt.Printf("Error: %v\n", err)
}
// Test declined payment
if err := simulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT"); err != nil {
fmt.Printf("Error: %v\n", err)
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Jose;
namespace ModulusWebhooks;
class Program
{
static async Task Main(string[] args)
{
var qrBody = "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000705033015204601653036085406100.005802PH5912MAIN ACCOUNT6006MANILA630412C0";
// Test successful payment simulation
await SimulateWebhook(qrBody, "SUCCESS");
// Test declined payment simulation
await SimulateWebhook(qrBody, "MISSING_DESTINATION_ACCOUNT");
}
static byte[] Base64UrlDecode(string base64Url)
{
var base64 = base64Url
.Replace('-', '+')
.Replace('_', '/');
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
return Convert.FromBase64String(base64);
}
static async Task SimulateWebhook(string qrBody, string useCase)
{
var secretKey = Environment.GetEnvironmentVariable("MODULUS_SECRET_KEY");
var encryptionKey = Environment.GetEnvironmentVariable("MODULUS_ENCRYPTION_KEY");
if (string.IsNullOrEmpty(secretKey) || string.IsNullOrEmpty(encryptionKey))
{
Console.WriteLine("Error: MODULUS_SECRET_KEY and MODULUS_ENCRYPTION_KEY must be set");
return;
}
try
{
// 1. Prepare and encrypt the payload
var payload = new { qrBody, useCase };
var key = Base64UrlDecode(encryptionKey);
var payloadJson = JsonSerializer.Serialize(payload);
var jweToken = JWT.Encode(payloadJson, key, JweAlgorithm.A256KW, JweEncryption.A256CBC_HS512);
// 2. Make API request with Basic Auth
using var client = new HttpClient();
var authBytes = Encoding.ASCII.GetBytes($"{secretKey}:");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authBytes));
var requestBody = new { request = new { Token = jweToken } };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
Encoding.UTF8,
"application/json"
);
var response = await client.PostAsync(
"https://webhooks.sbx.moduluslabs.io/v1/webhooks/qrph/simulate",
content
);
var responseBody = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"{useCase} webhook simulation sent successfully");
Console.WriteLine($"Response: {responseBody}");
}
else
{
Console.WriteLine($"Error: HTTP {(int)response.StatusCode}");
Console.WriteLine($"Details: {responseBody}");
}
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
}
}
Testing Workflow
Register Webhook
const webhook = await createWebhook(
'https://your-test-server.com/webhooks/modulus',
['QRPH_SUCCESS', 'QRPH_DECLINED'],
'ENABLED'
);
Simulate Success Event
await simulateWebhook('SUCCESS');
Simulate Declined Event
await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
Test Error Scenarios
- Return 5xx status to trigger retries
- Delay response beyond 10 seconds to test timeouts
- Verify idempotency by sending duplicate webhooks
Verify Decryption
Use Cases
Initial Integration Testing
Initial Integration Testing
console.log('Testing webhook integration...');
// Test success case
await simulateWebhook('SUCCESS');
await wait(2000);
// Test decline case
await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
await wait(2000);
console.log('Webhook integration test complete');
CI/CD Automated Testing
CI/CD Automated Testing
describe('Webhook Integration', () => {
it('should handle successful payments', async () => {
const result = await simulateWebhook('SUCCESS');
expect(result.webhooksSent).toBeGreaterThan(0);
// Wait for webhook to be processed
await wait(3000);
// Verify order was marked as paid
const order = await db.orders.findOne({ status: 'paid' });
expect(order).toBeDefined();
});
it('should handle declined payments', async () => {
const result = await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
expect(result.webhooksSent).toBeGreaterThan(0);
// Verify order was marked as failed
await wait(3000);
const order = await db.orders.findOne({ status: 'payment_failed' });
expect(order).toBeDefined();
});
});
Verify Decryption Implementation
Verify Decryption Implementation
// Send simulated webhook
await simulateWebhook('SUCCESS');
// Check server logs to verify decryption succeeded
console.log('Check your webhook endpoint logs for:');
console.log('- JWE decryption success message');
console.log('- Decrypted payload structure');
console.log('- Webhook processing result');
Test Error Handling
Test Error Handling
// Test idempotency by sending same webhook twice
await simulateWebhook('SUCCESS');
await wait(1000);
await simulateWebhook('SUCCESS');
// Verify duplicate webhook was ignored
// (should only process once)
Demo and Presentations
Demo and Presentations
console.log('Starting live webhook demo...');
// Simulate successful payment
console.log('Simulating successful payment...');
await simulateWebhook('SUCCESS');
await wait(2000);
// Simulate declined payment
console.log('Simulating declined payment...');
await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
console.log('Demo complete!');
Testing Checklist
Setup
- Webhook endpoint is accessible
- Webhook is registered and enabled
- Webhook actions include
QRPH_SUCCESSandQRPH_DECLINED
Test Successful Payment
- Simulate SUCCESS webhook
- Webhook received at endpoint
- JWE payload decrypted successfully
- Order marked as paid
- Confirmation email sent
Test Declined Payment
- Simulate declined webhook (e.g.,
MISSING_DESTINATION_ACCOUNT) - Webhook received at endpoint
- Decline reason parsed correctly
- Order marked as failed
- Customer notified
Test Error Handling
- Duplicate webhooks ignored (idempotency)
- Invalid webhooks rejected
- Timeout scenarios handled
- Retry mechanism tested
Verify Logging
- All webhooks logged
- Processing results recorded
- Errors captured
Best Practices
Test Both Scenarios
async function runFullWebhookTest() {
// Test success
await simulateWebhook('SUCCESS');
await verifySuccessHandling();
// Test declined
await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
await verifyDeclineHandling();
console.log('All webhook tests passed');
}
Use in CI/CD
# .github/workflows/test.yml
- name: Test Webhook Integration
run: |
npm run start:test-server &
sleep 5
npm run test:webhooks
Monitor Test Results
const testResults = {
successTests: 0,
declinedTests: 0,
failures: 0
};
try {
await simulateWebhook('SUCCESS');
testResults.successTests++;
} catch (error) {
testResults.failures++;
}
console.log('Test results:', testResults);
Test with Delays
await simulateWebhook('SUCCESS');
await wait(2000); // Wait 2 seconds
await simulateWebhook('MISSING_DESTINATION_ACCOUNT');
await wait(2000);
// Now verify results
Differences from Real Webhooks
| Aspect | Simulate API | Real Webhooks |
|---|---|---|
| Trigger | Manual API call | Actual QR Ph transaction |
| Reference Number | SIM-REF-* prefix | Real reference from QR creation |
| Transaction Data | Test data | Real customer payment data |
| Timing | Immediate | Depends on payment processing |
| Retries | Same as real | Up to 4 attempts (0, 15, 30, 45 min) |
| Encryption | JWE encrypted | JWE encrypted |
| Availability | Sandbox only | Sandbox and production |
Troubleshooting
404: No Enabled Webhooks Found
404: No Enabled Webhooks Found
- No webhooks registered
- All webhooks are disabled
- Webhook actions don’t include the simulated event
// Check existing webhooks
const webhooks = await getWebhooks();
console.log('Webhooks:', webhooks);
// Ensure at least one is enabled
const enabled = webhooks.filter(w => w.status === 'ENABLED');
if (enabled.length === 0) {
// Create or enable a webhook
await createWebhook(
'https://your-server.com/webhooks',
['QRPH_SUCCESS', 'QRPH_DECLINED'],
'ENABLED'
);
}
Webhook Not Received
Webhook Not Received
- Webhook URL not accessible
- Firewall blocking requests
- Endpoint not listening
- Test webhook URL accessibility externally
- Check server logs for incoming requests
- Verify endpoint is running and listening
- Test with ngrok or similar during development
Decryption Fails
Decryption Fails
- Wrong secret key
- JWE library issue
- Incorrect algorithm configuration
// Verify secret key
console.log('Secret key length:', SECRET_KEY.length);
console.log('Secret key starts with:', SECRET_KEY.substring(0, 3));
// Test decryption
try {
const payload = await decryptWebhook(webhookData, SECRET_KEY);
console.log('Decryption successful:', payload);
} catch (error) {
console.error('Decryption failed:', error);
}
Multiple Webhooks Sent
Multiple Webhooks Sent
const result = await simulateWebhook('SUCCESS');
console.log(`Webhooks sent: ${result.webhooksSent}`);
// This tells you how many webhooks received the notification
Next Steps
Create Webhook
Receiving Webhooks
Payload Structure
Setup Guide
Authorizations
HTTP Basic Authentication using your Secret Key as the username and an empty password
Body
Show child attributes
Show child attributes
Response
Webhook simulation sent successfully
The unique identifier of the original transaction (i.e., the id returned from the Create Dynamic QR Ph response).
Was this page helpful?