Powerful QRIS payment processing with real-time webhooks, comprehensive API, and beautiful merchant dashboard.
Real-time payment processing with instant webhooks and QRIS generation in milliseconds.
HMAC-SHA256 signature verification, API key authentication, and encrypted data transmission.
Modern merchant and admin dashboard with real-time statistics and transaction management.
Automatic retry mechanism, signature verification, and detailed webhook logs.
Dynamic QRIS generation with unique codes and automatic amount verification.
RESTful API, comprehensive documentation, and code examples in multiple languages.
Welcome to MSPay! Follow these steps to integrate our payment gateway into your application.
Contact the administrator to create a merchant account. You will receive:
Production: https://pay.masanto.me/api/v1
Development: http://localhost:3000/api/v1
MSPay uses API Key authentication. Include your API Key in the X-API-Key header for all requests.
X-API-Key: pk_fb6dfa91d532c8de3da3194429ddfa9af029207bc136b591d5e022bbbcf8573f
Content-Type: application/json
| Header | Required | Description |
|---|---|---|
X-API-Key |
✅ Yes | Your merchant API Key |
Content-Type |
✅ Yes | Must be application/json |
Create a new payment transaction and generate a QRIS code for your customer.
POST /api/v1/payment/create
curl -X POST http://localhost:3000/api/v1/payment/create \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_your_api_key_here" \
-d '{
"amount": 100000,
"customer_name": "John Doe",
"customer_phone": "081234567890",
"product_name": "Premium Package",
"payment_method": "qris"
}'
const response = await fetch('http://localhost:3000/api/v1/payment/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'pk_your_api_key_here'
},
body: JSON.stringify({
amount: 100000,
customer_name: 'John Doe',
customer_phone: '081234567890',
product_name: 'Premium Package',
payment_method: 'qris'
})
});
const data = await response.json();
console.log(data);
import requests
response = requests.post(
'http://localhost:3000/api/v1/payment/create',
headers={
'Content-Type': 'application/json',
'X-API-Key': 'pk_your_api_key_here'
},
json={
'amount': 100000,
'customer_name': 'John Doe',
'customer_phone': '081234567890',
'product_name': 'Premium Package',
'payment_method': 'qris'
}
)
data = response.json()
print(data)
<?php
$ch = curl_init('http://localhost:3000/api/v1/payment/create');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: pk_your_api_key_here'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'amount' => 100000,
'customer_name' => 'John Doe',
'customer_phone' => '081234567890',
'product_name' => 'Premium Package',
'payment_method' => 'qris'
]));
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
print_r($data);
?>
| Parameter | Type | Required | Description |
|---|---|---|---|
amount |
integer | ✅ Yes | Amount in IDR (minimum: 1000) |
customer_name |
string | ❌ No | Customer name |
customer_phone |
string | ❌ No | Customer phone number |
product_name |
string | ❌ No | Product/service name |
payment_method |
string | ❌ No | Payment method (default: "qris") |
{
"success": true,
"data": {
"transaction_id": "TRX1736789012345ABC",
"payment_url": "http://localhost:3000/payment/?id=TRX1736789012345ABC",
"qris_string": "00020101021126...",
"qr_image": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"amount": 100000,
"unique_code": 456,
"total_amount": 100456,
"payment_method": "qris",
"expires_at": "2026-01-07T12:30:00.000Z",
"status": "pending"
}
}
unique_code is automatically added to help identify payments. Your customer must pay the exact total_amount.
Check the current status of a payment transaction.
GET /api/v1/payment/{transaction_id}
curl -X GET http://localhost:3000/api/v1/payment/TRX1736789012345ABC \
-H "X-API-Key: pk_your_api_key_here"
{
"success": true,
"data": {
"transaction_id": "TRX1736789012345ABC",
"status": "paid",
"amount": 100000,
"total_amount": 100456,
"unique_code": 456,
"customer_name": "John Doe",
"customer_phone": "081234567890",
"product_name": "Premium Package",
"payment_method": "qris",
"paid_at": "2026-01-07T12:15:30.000Z",
"created_at": "2026-01-07T12:00:00.000Z",
"expires_at": "2026-01-07T12:30:00.000Z"
}
}
| Status | Description |
|---|---|
pending |
Payment is waiting for customer to pay |
paid |
Payment has been successfully completed |
expired |
Payment link has expired (default: 30 minutes) |
cancelled |
Payment was cancelled |
MSPay automatically sends webhooks to your server when a payment status changes. This is the recommended way to handle payment notifications.
https://yourstore.com/api/payment/callbackhttps://abc123.ngrok.io/webhook/mspay (for testing)
When a payment is completed, MSPay will send a POST request to your webhook URL:
POST https://yourstore.com/api/payment/callback
Headers:
X-Signature: a8f5f167f44f4964e6c998dee827110c284831e5f54e47e5b7b7c20f3eef4d4e
Content-Type: application/json
Body:
{
"transaction_id": "TRX1736789012345ABC",
"merchant_id": "MERCH12345",
"status": "paid",
"amount": 100000,
"unique_code": 456,
"total_amount": 100456,
"customer_name": "John Doe",
"customer_phone": "081234567890",
"product_name": "Premium Package",
"paid_at": "2026-01-07T12:15:30.000Z",
"payment_method": "qris"
}
CRITICAL: Always verify webhook signatures to prevent fraud and ensure requests are genuinely from MSPay.
X-Signature headerconst express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const API_SECRET = process.env.MSPAY_API_SECRET; // sk_xxx...
app.post('/api/payment/callback', (req, res) => {
// 1. Get signature from header
const signature = req.headers['x-signature'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// 2. Get payload
const payload = req.body;
// 3. Generate expected signature
const expectedSignature = crypto
.createHmac('sha256', API_SECRET)
.update(JSON.stringify(payload))
.digest('hex');
// 4. Verify signature (timing-safe comparison)
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
if (!isValid) {
console.error('Invalid signature!');
return res.status(401).json({ error: 'Invalid signature' });
}
// 5. ✅ Signature valid - process payment
console.log('✅ Valid webhook received:', payload);
// Update order in your database
const { transaction_id, status, total_amount } = payload;
if (status === 'paid') {
// Mark order as paid
// Send confirmation email
// Update inventory
// etc.
}
// 6. Return success response
res.json({ success: true });
});
app.listen(5000, () => {
console.log('Webhook server running on port 5000');
});
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
API_SECRET = os.getenv('MSPAY_API_SECRET') # sk_xxx...
@app.route('/api/payment/callback', methods=['POST'])
def mspay_callback():
# 1. Get signature from header
signature = request.headers.get('X-Signature')
if not signature:
return jsonify({'error': 'Missing signature'}), 401
# 2. Get payload
payload = request.get_json()
# 3. Generate expected signature
payload_string = json.dumps(payload, separators=(',', ':'))
expected_signature = hmac.new(
API_SECRET.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 4. Verify signature (timing-safe comparison)
is_valid = hmac.compare_digest(signature, expected_signature)
if not is_valid:
print('❌ Invalid signature!')
return jsonify({'error': 'Invalid signature'}), 401
# 5. ✅ Signature valid - process payment
print('✅ Valid webhook received:', payload)
transaction_id = payload.get('transaction_id')
status = payload.get('status')
total_amount = payload.get('total_amount')
if status == 'paid':
# Mark order as paid
# Send confirmation email
# Update inventory
pass
# 6. Return success response
return jsonify({'success': True}), 200
if __name__ == '__main__':
app.run(port=5000)
<?php
$apiSecret = getenv('MSPAY_API_SECRET'); // sk_xxx...
// 1. Get signature from header
$headers = getallheaders();
$signature = $headers['X-Signature'] ?? '';
if (empty($signature)) {
http_response_code(401);
echo json_encode(['error' => 'Missing signature']);
exit;
}
// 2. Get payload
$payload = json_decode(file_get_contents('php://input'), true);
// 3. Generate expected signature
$payloadString = json_encode($payload, JSON_UNESCAPED_SLASHES);
$expectedSignature = hash_hmac('sha256', $payloadString, $apiSecret);
// 4. Verify signature (timing-safe comparison)
$isValid = hash_equals($signature, $expectedSignature);
if (!$isValid) {
error_log('❌ Invalid signature!');
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// 5. ✅ Signature valid - process payment
error_log('✅ Valid webhook received: ' . json_encode($payload));
$transactionId = $payload['transaction_id'];
$status = $payload['status'];
$totalAmount = $payload['total_amount'];
if ($status === 'paid') {
// Mark order as paid
// Send confirmation email
// Update inventory
}
// 6. Return success response
http_response_code(200);
echo json_encode(['success' => true]);
?>
MSPay uses standard HTTP status codes and returns detailed error messages in JSON format.
{
"success": false,
"message": "Invalid API Key"
}
| Status Code | Meaning | Description |
|---|---|---|
200 |
OK | Request successful |
201 |
Created | Resource created successfully |
400 |
Bad Request | Invalid request parameters |
401 |
Unauthorized | Invalid or missing API Key |
404 |
Not Found | Transaction not found |
429 |
Too Many Requests | Rate limit exceeded |
500 |
Internal Server Error | Server error, please contact support |
| Error Message | Solution |
|---|---|
| Invalid API Key | Check your API Key in merchant dashboard |
| Minimum amount is Rp 1.000 | Amount must be at least 1000 |
| Transaction not found | Verify the transaction ID is correct |
| Invalid signature | Check your API Secret and payload format |
| Merchant is suspended | Contact admin to activate your account |
Here's a complete example of integrating MSPay into an e-commerce checkout:
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const MSPAY_API_URL = 'http://localhost:3000/api/v1';
const MSPAY_API_KEY = process.env.MSPAY_API_KEY;
const MSPAY_API_SECRET = process.env.MSPAY_API_SECRET;
// 1. Create Order & Payment
app.post('/checkout', async (req, res) => {
try {
const { cart, customer } = req.body;
// Calculate total
const amount = cart.reduce((sum, item) => sum + item.price, 0);
// Create payment via MSPay
const response = await axios.post(
`${MSPAY_API_URL}/payment/create`,
{
amount: amount,
customer_name: customer.name,
customer_phone: customer.phone,
product_name: cart.map(i => i.name).join(', '),
payment_method: 'qris'
},
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': MSPAY_API_KEY
}
}
);
const payment = response.data.data;
// Save order to database
const order = await db.orders.create({
transaction_id: payment.transaction_id,
customer_id: customer.id,
amount: payment.total_amount,
status: 'pending',
items: cart
});
// Return payment info to frontend
res.json({
success: true,
order_id: order.id,
payment_url: payment.payment_url,
qr_image: payment.qr_image,
amount: payment.total_amount,
expires_at: payment.expires_at
});
} catch (error) {
console.error('Checkout error:', error);
res.status(500).json({ error: 'Checkout failed' });
}
});
// 2. Receive Webhook from MSPay
app.post('/webhook/mspay', async (req, res) => {
try {
// Verify signature
const signature = req.headers['x-signature'];
const payload = req.body;
const expectedSignature = crypto
.createHmac('sha256', MSPAY_API_SECRET)
.update(JSON.stringify(payload))
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Find order
const order = await db.orders.findOne({
transaction_id: payload.transaction_id
});
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// Update order status
if (payload.status === 'paid' && order.status === 'pending') {
await db.orders.update(order.id, {
status: 'paid',
paid_at: payload.paid_at
});
// Send confirmation email
await sendEmail({
to: order.customer.email,
subject: 'Payment Confirmed',
template: 'payment-success',
data: { order, payment: payload }
});
// Update inventory
for (const item of order.items) {
await db.products.decrementStock(item.id, item.quantity);
}
console.log(`✅ Order ${order.id} marked as paid`);
}
res.json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal error' });
}
});
// 3. Check Payment Status (polling)
app.get('/order/:orderId/status', async (req, res) => {
try {
const order = await db.orders.findById(req.params.orderId);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// Check with MSPay if still pending
if (order.status === 'pending') {
const response = await axios.get(
`${MSPAY_API_URL}/payment/${order.transaction_id}`,
{
headers: { 'X-API-Key': MSPAY_API_KEY }
}
);
const payment = response.data.data;
if (payment.status !== order.status) {
await db.orders.update(order.id, { status: payment.status });
order.status = payment.status;
}
}
res.json({
order_id: order.id,
status: order.status,
amount: order.amount
});
} catch (error) {
console.error('Status check error:', error);
res.status(500).json({ error: 'Failed to check status' });
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
ngrok http 3000
https://abc123.ngrok.io/webhook/mspay