Introduction v5.3.1
Powering global payments with secure, enterprise-grade APIs.
This documentation describes the integration procedures and POST protocol usage for e-commerce merchants using the Lipapap Enterprise Payment Platform. Our API implements acquiring payments (purchases) with a robust, secure, and scalable interaction model.
Key Capabilities
- Global Payments: Support for 20+ currencies and alternative payment methods (APMs).
- Recurring & Automation: Tokenization, scheduled payments, and Card2Card transfers.
- Enterprise Security: 3D-Secure, HMAC-SHA256 signatures, and PCI DSS Level 1 compliance.
- Developer Friendly: Clear request/response models, webhooks, and sandbox testing.
Integration Process
Get started in four simple steps.
1. Merchant Registration
Before accessing the Payment Platform, provide the following data to your account manager:
| Data | Description |
|---|---|
IP list | List of IP addresses from which requests will be sent. |
Callback URL | URL for receiving processing result notifications. Max 255 characters. |
Contact email | Email of the person monitoring transactions and refunds. |
After registration, you will receive your credentials:
| Credential | Description |
|---|---|
CLIENT_KEY | Unique UUID to identify your account. |
SECRET_KEY | Private key for signature generation. Keep confidential. |
PAYMENT_URL | Endpoint URL for API requests. |
2. Protocol Mapping
Ensure the S2S CARD protocol is mapped in your account. Payments cannot be processed without proper mapping.
3. API Interaction
Send HTTPS POST requests to PAYMENT_URL with the required parameters. All responses are JSON-encoded.
3D-Secure (3DS) Flow
For 3DS-enabled transactions, the API returns a redirect response. You must redirect the cardholder to the provided redirect_url with the redirect_params using the specified redirect_method (POST/GET). After verification, the cardholder returns to your term_url_3ds.
{
"action": "SALE",
"client_key": "9e48fe5c-7653-824c-71a3-ad155d18ad27",
"order_id": "order-12345",
"order_amount": "99.99",
"order_currency": "USD",
"term_url_3ds": "https://your-site.com/3ds-return",
"hash": "a1b2c3d4..."
}
{
"action": "SALE",
"result": "REDIRECT",
"status": "3DS",
"trans_id": "txn-67890",
"redirect_url": "https://acs.bank.com/3ds",
"redirect_method": "POST",
"redirect_params": { "PaReq": "...", "MD": "..." }
}
4. Possible Actions
| Action | Description |
|---|---|
SALE | Creates a SALE or AUTH transaction. |
CAPTURE | Captures a previously authorized transaction. |
CREDITVOID | Creates a REFUND or REVERSAL. |
RECURRING_SALE | Creates a SALE using stored cardholder data. |
CARD2CARD | Transfers funds between two credit cards. |
GET_TRANS_STATUS | Retrieves the status of a transaction. |
Sale / Auth / Capture
Single Message (SMS) and Dual Message (DMS) transactions.
SALE Request
Use auth=Y for AUTH transactions (DMS). For sub-accounts, specify channel_id.
Request Parameters
| Parameter | Description | Required |
|---|---|---|
action | SALE | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID (max 255 chars) | Yes |
order_amount | Amount (e.g., 100.99) | Yes |
order_currency | 3-letter currency code (e.g., USD) | Yes |
card_number | Credit card number | Yes* |
card_exp_month | Expiry month (MM) | Yes* |
card_exp_year | Expiry year (YYYY) | Yes* |
card_cvv2 | CVV/CVC2 | Yes |
payer_email | Customer email | Yes |
payer_ip | Customer IP address | Yes |
term_url_3ds | 3DS return URL | Yes |
auth | Y for AUTH only | No |
hash | Signature (Formula 1) | Yes |
* Optional if card_token is provided.
Response (Successful Sale)
{
"action": "SALE",
"result": "SUCCESS",
"status": "SETTLED",
"order_id": "order-12345",
"trans_id": "txn-67890",
"amount": "99.99",
"currency": "USD",
"descriptor": "LIPAPAP*Merchant"
}
{
"action": "SALE",
"result": "DECLINED",
"status": "DECLINED",
"order_id": "order-12345",
"trans_id": "txn-67890",
"decline_reason": "Insufficient funds"
}
CAPTURE Request
Captures a previously authorized transaction (auth=Y).
| Parameter | Description | Required |
|---|---|---|
action | CAPTURE | Yes |
client_key | Your unique UUID | Yes |
trans_id | Platform transaction ID from the AUTH | Yes |
amount | Capture amount (optional, defaults to full amount) | No |
hash | Signature (Formula 2) | Yes |
Alternative Payments (APM)
Mobile money, e-wallets, and local payment methods.
Processes payments using mobile money and alternative payment methods. Supports 20+ currencies.
payer_phone is valid for the selected country and mobile money provider.
Request Parameters
| Parameter | Description | Required |
|---|---|---|
action | MOMOAPM | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount (e.g., 113.00) | Yes |
order_description | Transaction description | Yes |
payer_phone | Customer phone (e.g., +2547XXXXXXXX) | Yes |
payer_email | Customer email | Yes |
payer_country | 2-letter country code (e.g., KE) | Yes |
term_url_3ds | Return URL after payment | Yes |
timestamp | Unix timestamp (milliseconds) | Yes |
hash | Signature (Formula 1) | Yes |
Sample Request
{
"action": "MOMOAPM",
"client_key": "fe5cad15-5763-4c82-a371-9e488ad275d1",
"order_id": "pay-1643-11001",
"order_amount": "113.00",
"order_description": "Payment for Invoice #INV-001",
"payer_phone": "254712345678",
"payer_email": "customer@example.com",
"payer_country": "KE",
"term_url_3ds": "https://example.com/callback",
"timestamp": "1741027351153",
"hash": "6517839f60ca23c382d02e9dbcfa82f8"
}
{
"action": "MOMOAPM",
"result": "REDIRECT",
"status": "REDIRECT",
"order_id": "pay-1643-11001",
"trans_id": "pay-1643-11001",
"amount": "113.00",
"currency": "PHP",
"txMsg": "Transaction successful",
"redirect_url": "https://lipapap.net/pgate/refer.php?token=...",
"redirect_method": "GET"
}
Simulates a Customer to Business (C2B) payment transaction. This is intended for sandbox/testing environments only and allows developers to verify the C2B payment flow without using real funds.
C2B_SIMULATE will be rejected.
Request Parameters
| Parameter | Description | Required |
|---|---|---|
action | C2B_SIMULATE | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount in KES (e.g., 10.00) | Yes |
order_description | Transaction description | Yes |
payer_phone | Customer phone (format: 2547XXXXXXXX) | Yes |
payer_email | Customer email | Yes |
payer_country | 2-letter country code (e.g., KE) | Yes |
timestamp | Unix timestamp (milliseconds) | Yes |
hash | Signature (Formula 1) | Yes |
Sample Request
{
"action": "C2B_SIMULATE",
"client_key": "fe5cad15-5763-4c82-a371-9e488ad275d1",
"order_id": "SIM-2026-001234",
"order_amount": "10.00",
"order_description": "C2B Simulation Test",
"payer_phone": "254712345678",
"payer_email": "simulate@example.com",
"payer_country": "KE",
"timestamp": "1741027351153",
"hash": "6517839f60ca23c382d02e9dbcfa82f8"
}
{
"action": "C2B_SIMULATE",
"status": "success",
"environment": "sandbox",
"order_id": "SIM-2026-001234",
"message": "Success. Request accepted for processing",
"ResponseCode": "0",
"ResponseDescription": "Success",
"TransactionID": "SIM123456789"
}
Initiates an STK (Sim Toolkit) Push payment request. The customer receives a prompt on their mobile phone to enter their M-PESA PIN and complete the transaction.
Request Parameters
| Parameter | Description | Required |
|---|---|---|
action | STK_PUSH | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount in KES (e.g., 100.00) | Yes |
order_description | Transaction description | Yes |
payer_phone | Customer phone (format: 2547XXXXXXXX) | Yes |
payer_email | Customer email | Yes |
payer_country | 2-letter country code (e.g., KE) | Yes |
term_url_3ds | Callback URL for transaction result | Yes |
timestamp | Unix timestamp (milliseconds) | Yes |
hash | Signature (Formula 1) | Yes |
Sample Request
{
"action": "STK_PUSH",
"client_key": "fe5cad15-5763-4c82-a371-9e488ad275d1",
"order_id": "Mpesa-5643-12358",
"order_amount": "100.00",
"order_description": "Deposit From M-PESA",
"payer_phone": "254722905699",
"payer_email": "customer@example.com",
"payer_country": "KE",
"term_url_3ds": "https://example.com/callback",
"timestamp": "1741027351153",
"hash": "6517839f60ca23c382d02e9dbcfa82f8"
}
{
"action": "STK_PUSH",
"status": "success",
"environment": "production",
"order_id": "Mpesa-5643-12358",
"CheckoutRequestID": "ws_CO_123456789",
"MerchantRequestID": "MR-12345-67890",
"ResponseCode": "0",
"ResponseDescription": "Success. Request accepted for processing",
"message": "STK Push sent successfully. Awaiting customer confirmation."
}
STK Push Flow
- Your system sends the
STK_PUSHrequest to Lipapap. - Lipapap forwards the request to the M-PESA API.
- The customer receives a pop-up on their phone asking them to confirm the payment.
- After the customer enters their PIN, M-PESA processes the transaction.
- A callback is sent to your
term_url_3dswith the final transaction result.
This action is used to handle incoming confirmation callbacks from the M-PESA API for C2B transactions. This endpoint is called by Lipapap/M-PESA and should be implemented by your system to receive and process C2B transaction confirmations.
Callback Payload (Received from Lipapap)
| Field | Description |
|---|---|
TransID | M-PESA transaction ID |
TransactionType | Type of transaction (e.g., PayBill, BuyGoods) |
TransTime | Transaction timestamp |
TransAmount | Transaction amount |
BusinessShortCode | Your Paybill/Till number |
BillRefNumber | Bill reference number |
MSISDN | Customer phone number |
FirstName | Customer first name |
LastName | Customer last name |
Sample Callback Payload
{
"TransID": "TSC123456789",
"TransactionType": "PayBill",
"TransTime": "20260128153045",
"TransAmount": "100.00",
"BusinessShortCode": "123456",
"BillRefNumber": "INV-2026-001",
"InvoiceNumber": "INV-2026-001",
"OrgAccountBalance": "50000.00",
"ThirdPartyTransID": "TP-123456",
"MSISDN": "254712345678",
"FirstName": "James",
"MiddleName": "M",
"LastName": "Kilali"
}
{
"ResultCode": "0",
"ResultDesc": "Success"
}
Webhook Implementation Example (Server-Side)
<?php // Your webhook endpoint to receive C2B confirmations // Example: https://your-domain.com/mpesa/c2b/callback // Get the raw POST data from M-PESA $rawInput = file_get_contents('php://input'); $data = json_decode($rawInput, true); // Validate the callback if (!$data) { http_response_code(400); echo json_encode(['ResultCode' => '1', 'ResultDesc' => 'Invalid payload']); exit; } // Extract transaction data $transactionId = $data['TransID'] ?? null; $amount = $data['TransAmount'] ?? 0; $phone = $data['MSISDN'] ?? ''; $billRef = $data['BillRefNumber'] ?? ''; $shortcode = $data['BusinessShortCode'] ?? ''; // Process the transaction in your system // 1. Validate the bill reference // 2. Update order status // 3. Send confirmation to customer // Respond to M-PESA with success http_response_code(200); echo json_encode([ 'ResultCode' => '0', 'ResultDesc' => 'Success' ]); ?>
ResultCode: 0 within 5 seconds, otherwise M-PESA will retry the callback.
Refund / Reverse
CreditVOID transactions for full or partial refunds.
CREDITVOID Request
Completes a REFUND (full/partial) or REVERSAL transaction.
| Parameter | Description | Required |
|---|---|---|
action | CREDITVOID | Yes |
client_key | Your unique UUID | Yes |
trans_id | Platform transaction ID to refund | Yes |
amount | Refund amount (optional, defaults to full) | No |
hash | Signature (Formula 2) | Yes |
Response
{
"action": "CREDITVOID",
"result": "ACCEPTED",
"order_id": "order-12345",
"trans_id": "txn-67890"
}
{
"action": "CREDITVOID",
"result": "SUCCESS",
"status": "REFUND",
"order_id": "order-12345",
"trans_id": "txn-67890",
"amount": "99.99",
"hash": "a1b2c3d4..."
}
Void / Debit / Credit2Card
Cancel, debit, or transfer funds.
VOID Request
Cancels a previously authorized transaction.
| Parameter | Description | Required |
|---|---|---|
action | VOID | Yes |
client_key | Your unique UUID | Yes |
trans_id | Platform transaction ID to void | Yes |
hash | Signature (Formula 2) | Yes |
DEBIT Request
Transfers funds from a credit card to a merchant's account.
| Parameter | Description | Required |
|---|---|---|
action | DEBIT | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount (e.g., 500.00) | Yes |
card_number | Credit card number | Yes |
card_exp_month | Expiry month | Yes |
card_exp_year | Expiry year | Yes |
payer_ip | Customer IP address | Yes |
hash | Signature (Formula 1) | Yes |
CREDIT2CARD Request
Transfers funds from a merchant's account to a credit card.
| Parameter | Description | Required |
|---|---|---|
action | CREDIT2CARD | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount (e.g., 250.00) | Yes |
card_number | Receiver's credit card number | Yes |
card_exp_month | Receiver's expiry month | Yes |
card_exp_year | Receiver's expiry year | Yes |
payer_ip | Customer IP address | Yes |
hash | Signature (Formula 1) | Yes |
Recurring Sale & Card2Card
Automated payments and card-to-card transfers.
RECURRING_SALE Request
Implements SALE or AUTH transactions using a stored recurring_token from a previous transaction with recurring_init=Y.
| Parameter | Description | Required |
|---|---|---|
action | RECURRING_SALE | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount | Yes |
recurring_token | Token from a previous transaction | Yes |
payer_email | Customer email | Yes |
payer_ip | Customer IP address | Yes |
auth | Y for AUTH only | No |
hash | Signature (Formula 1) | Yes |
CARD2CARD Request
Transfers money between two credit cards.
| Parameter | Description | Required |
|---|---|---|
action | CARD2CARD | Yes |
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount | Yes |
sender_card_number | Sender's card number | Yes |
sender_card_exp_month | Sender's expiry month | Yes |
sender_card_exp_year | Sender's expiry year | Yes |
sender_card_cvv2 | Sender's CVV | Yes |
receiver_card_number | Receiver's card number | Yes |
payer_ip | Customer IP address | Yes |
term_url_3ds | 3DS return URL | Yes |
hash | Signature (Formula 1) | Yes |
Recurring Schedules
CREATE, PAUSE, RUN, DELETE, and INFO operations.
CREATE_SCHEDULE Request
Creates a recurring payment schedule.
| Parameter | Description | Required |
|---|---|---|
action | CREATE_SCHEDULE | Yes |
client_key | Your unique UUID | Yes |
schedule_id | Unique schedule ID | Yes |
recurring_token | Token from a previous transaction | Yes |
amount | Amount per payment | Yes |
currency | 3-letter currency code | Yes |
start_date | Schedule start date (yyyy-MM-dd) | Yes |
interval | DAILY, WEEKLY, MONTHLY, YEARLY | Yes |
interval_count | Number of intervals between payments | Yes |
hash | Signature (Formula 1) | Yes |
Schedule Management
| Action | Description | Required Params |
|---|---|---|
PAUSE_SCHEDULE | Suspends a schedule | schedule_id |
RUN_SCHEDULE | Resumes a paused schedule | schedule_id |
DELETE_SCHEDULE | Deletes a schedule | schedule_id |
SCHEDULE_INFO | Retrieves schedule details | schedule_id |
DESCHEDULE | Terminates schedule and removes token | schedule_id |
All management actions require client_key, action, schedule_id, and hash (Formula 2).
Transaction Status
Query transaction and order details.
GET_TRANS_STATUS
Retrieves the status of a specific transaction.
| Parameter | Description | Required |
|---|---|---|
action | GET_TRANS_STATUS | Yes |
client_key | Your unique UUID | Yes |
trans_id | Platform transaction ID | Yes |
hash | Signature (Formula 2) | Yes |
GET_TRANS_DETAILS
Retrieves full order details including all associated transactions.
| Parameter | Description | Required |
|---|---|---|
action | GET_TRANS_DETAILS | Yes |
client_key | Your unique UUID | Yes |
order_id | Merchant transaction ID | Yes |
hash | Signature (Formula 2) | Yes |
Status Codes
| Status | Description |
|---|---|
SETTLED | Transaction completed successfully. |
PENDING | Awaiting capture (AUTH transaction). |
DECLINED | Transaction was declined. |
3DS | Awaiting 3D-Secure validation. |
REFUND | Transaction has been refunded. |
REVERSAL | Transaction has been reversed. |
VOID | Transaction has been voided. |
Appendix & Error Codes
Signature formulas, error codes, and testing.
Formula 1: Request Signature
Signature = HMAC-SHA256(
CONCAT(
client_key,
order_id,
order_amount,
order_currency,
order_description,
card_number,
card_exp_month,
card_exp_year,
payer_email,
payer_phone,
payer_ip,
secret_key
),
secret_key
)
Non-required parameters not provided should be replaced with an empty string.
Formula 2: Response/Callback Signature
Signature = HMAC-SHA256(
CONCAT(
action,
result,
status,
order_id,
trans_id,
trans_date,
amount,
currency,
decline_reason,
secret_key
),
secret_key
)
If decline_reason is not present, use an empty string.
Error Codes
| Code | Description |
|---|---|
100 | Invalid client_key |
101 | Invalid signature |
102 | Missing required parameter |
200 | Transaction not found |
202 | Transaction declined by issuer |
300 | Invalid card details |
302 | Insufficient funds |
500 | Token not found |
600 | System error โ contact support |
Testing
| Card Number | Expiry | Result |
|---|---|---|
4111 1111 1111 1111 | 01/2026 | Successful SALE โ SETTLED |
4000 0000 0000 0002 | 01/2026 | Declined โ DECLINED |
5555 5555 5555 4444 | 01/2026 | 3DS Required โ REDIRECT |
iFrame Integration v2.0
Embed the Lipapap payment form directly in your website.
How It Works
You can embed the Lipapap payment interface in an iFrame by constructing the proper URL with query parameters. The iFrame will handle the payment flow (including 3DS) and communicate results via postMessage or redirects.
1. Build the iFrame URL
Use the following base URL and append the required parameters. All parameters are URL-encoded.
| Parameter | Description | Required |
|---|---|---|
client_key | Your unique UUID | Yes |
order_id | Your transaction ID | Yes |
order_amount | Amount (e.g., 99.99) | Yes |
order_currency | 3-letter currency code | Yes |
payer_email | Customer email | Yes |
payer_ip | Customer IP address | Yes |
term_url_3ds | 3DS return URL (must be same origin as parent) | Yes |
hash | Signature (Formula 1) | Yes |
iframe_mode | Set to 1 to enable iFrame mode | Yes |
redirect_method | POST or GET (default POST) | No |
Example iFrame URL:
https://api.lipapap.com/payment/iframe? client_key=9e48fe5c-7653-824c-71a3-ad155d18ad27 &order_id=order-12345 &order_amount=99.99 &order_currency=USD &payer_email=customer@example.com &payer_ip=192.168.1.100 &term_url_3ds=https://your-site.com/3ds-return &hash=a1b2c3d4e5f6... &iframe_mode=1
<iframe src="https://api.lipapap.com/payment/iframe?
client_key=9e48fe5c-7653-824c-71a3-ad155d18ad27
&order_id=order-12345
&order_amount=99.99
&order_currency=USD
&payer_email=customer@example.com
&payer_ip=192.168.1.100
&term_url_3ds=https://your-site.com/3ds-return
&hash=a1b2c3d4e5f6...
&iframe_mode=1"
width="100%"
height="600"
frameborder="0"
allow="payment"
sandbox="allow-scripts allow-same-origin allow-forms">
</iframe>
2. Handling 3DS in iFrame
When 3DS is required, the iFrame will redirect to the bank's ACS page. After verification, the cardholder is sent back to your term_url_3ds. For iFrame mode, we recommend that term_url_3ds sends a postMessage back to the parent window with the transaction result.
// In your parent page, listen for messages from the iFrame window.addEventListener('message', function(event) { // Verify origin for security if (event.origin !== 'https://api.lipapap.com') return; const data = event.data; if (data.type === 'payment_result') { // Handle success, decline, or error console.log('Payment result:', data); if (data.result === 'SUCCESS') { // Redirect to success page or update UI } else { // Handle decline } } });
// On your term_url_3ds page (inside iFrame): const result = { type: 'payment_result', result: 'SUCCESS', // or 'DECLINED' order_id: 'order-12345', trans_id: 'txn-67890', amount: '99.99' }; // Send result to parent window window.parent.postMessage(result, 'https://your-site.com');
3. iFrame Styling & Responsiveness
You can style the container and iFrame to fit your page design. We recommend using a flexible layout:
/* Container */ .payment-iframe-container { width: 100%; max-width: 480px; margin: 0 auto; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.08); background: #ffffff; } .payment-iframe-container iframe { width: 100%; height: 520px; border: none; display: block; } /* Responsive */ @media (max-width: 600px) { .payment-iframe-container iframe { height: 480px; } }
4. Full Integration Example
Below is a complete, working example that combines the iFrame, postMessage listener, and a simple UI.
<!DOCTYPE html>
<html>
<head>
<title>Lipapap iFrame Payment</title>
<style>
body { font-family: Inter, sans-serif; background: #f8fafc; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 20px; }
.container { width: 100%; max-width: 480px; }
.card { background: white; border-radius: 16px; padding: 24px; box-shadow: 0 4px 24px rgba(0,0,0,0.06); }
.card h2 { margin-top: 0; font-weight: 600; }
.amount { font-size: 1.5rem; font-weight: 700; color: #0f172a; }
.iframe-wrapper { margin: 20px 0; border-radius: 12px; overflow: hidden; border: 1px solid #e2e8f0; }
.iframe-wrapper iframe { width: 100%; height: 480px; border: none; display: block; }
.status { padding: 12px 16px; border-radius: 8px; margin-top: 16px; display: none; }
.status.success { background: #dcfce7; color: #166534; display: block; }
.status.declined { background: #fee2e2; color: #991b1b; display: block; }
.status.error { background: #fef9c3; color: #854d0e; display: block; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<h2>Complete Your Payment</h2>
<p>Order: <strong>order-12345</strong></p>
<p class="amount">$99.99 USD</p>
<div class="iframe-wrapper">
<iframe id="paymentIframe"
src="https://api.lipapap.com/payment/iframe?client_key=9e48fe5c-7653-824c-71a3-ad155d18ad27&order_id=order-12345&order_amount=99.99&order_currency=USD&payer_email=customer@example.com&payer_ip=192.168.1.100&term_url_3ds=https://your-site.com/3ds-return&hash=a1b2c3d4e5f6...&iframe_mode=1"
allow="payment"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</div>
<div id="paymentStatus" class="status"></div>
</div>
</div>
<script>
window.addEventListener('message', function(event) {
// Always verify origin in production
if (event.origin !== 'https://api.lipapap.com') return;
const data = event.data;
if (data.type === 'payment_result') {
const statusDiv = document.getElementById('paymentStatus');
if (data.result === 'SUCCESS') {
statusDiv.className = 'status success';
statusDiv.textContent = 'โ
Payment successful! Transaction ID: ' + data.trans_id;
} else if (data.result === 'DECLINED') {
statusDiv.className = 'status declined';
statusDiv.textContent = 'โ Payment declined: ' + (data.decline_reason || 'Please try again.');
} else {
statusDiv.className = 'status error';
statusDiv.textContent = 'โ ๏ธ Payment error: ' + (data.message || 'Unknown error');
}
}
});
</script>
</body>
</html>
event.origin in your postMessage listener to prevent malicious data injection. Also ensure your term_url_3ds page sends the payment result only after proper validation.