LworxPay Merchant API
Integrate LworxPay into your website or app and accept payments without redirecting your customers to another page. Two integration modes available: Direct Charge for Uganda mobile money, and Payment Link for all countries worldwide.
πΊπ¬ Mode 1: Direct Charge
Uganda only β MTN & Airtel mobile money. Customer never leaves your page. You call the API with phone + amount, customer approves on their phone.
- No redirect β customer stays on your site
- Works for Uganda (MTN MoMo & Airtel)
- Poll status or receive webhook on completion
- Simple: phone + amount β done
π Mode 2: Payment Link
All countries worldwide via Flutterwave. You initiate the payment, get a hosted checkout URL, redirect your customer. Supports cards, bank transfer, MoMo, and more.
- Works globally β all countries
- Cards, bank transfer, mobile money
- Hosted checkout β no PCI compliance needed
- Webhook on payment completion
Fast Integration
Get started in minutes. Simple REST API with comprehensive examples in PHP, Node.js, Python, and cURL.
Secure Webhooks
Bank-grade HMAC-SHA256 signature verification on every webhook notification. Prevent fraud and fake callbacks.
Instant Webhooks
Get notified instantly when payment is completed or failed. No polling required β your server gets called automatically.
Base URL
https://lworx.ug-web.com/api/v1Which Mode Should I Use?
| Scenario | Use |
|---|---|
| My customers are in Uganda and have MTN or Airtel | Direct Charge |
| I want customers to pay without leaving my page | Direct Charge |
| My customers are outside Uganda or use cards/bank | Payment Link |
| I want a hosted checkout page (no PCI burden) | Payment Link |
| WooCommerce / e-commerce store globally | Payment Link |
Authentication
LworxPay has two authentication methods depending on which integration mode you use. Get your credentials from the merchant dashboard under Merchant β API Configuration.
Sandbox vs Production
Always test in sandbox first. Sandbox credentials have a test_ prefix and process no real money. Switch to production credentials when you are ready to go live.
πΊπ¬ Direct Charge β Bearer Token
Used for Direct Charge API (Uganda mobile money). Pass your API Key as a Bearer token in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonCredentials needed: API Key only
π Payment Link β Header Keys
Used for Payment Link API (all countries). Pass Merchant Key and API Key as custom headers.
X-Merchant-Key: YOUR_MERCHANT_KEY
X-API-Key: YOUR_API_KEY
X-Environment: production
Content-Type: application/jsonCredentials needed: Merchant Key + API Key
Where to Find Your Credentials
- Log in to your LworxPay merchant dashboard
- Navigate to Merchant β API Configuration
- Copy your Merchant ID, API Key, and Client Secret
- For sandbox, toggle to Sandbox Mode to get
test_-prefixed credentials
Your Credentials Explained
| Credential | Used In | Description |
|---|---|---|
API Key |
Both modes | Your primary API authentication key. Used as Bearer token for Direct Charge, and as X-API-Key header for Payment Link. |
Merchant Key (Merchant ID) |
Payment Link only | Your unique merchant identifier. Passed as X-Merchant-Key header when initiating a payment. |
Client Secret |
Webhook verification | Used to verify webhook signatures. Never expose this publicly β keep it server-side only. |
Environments
Sandbox
X-Environment: sandbox
Credentials: test_ prefixed keys
Money: No real money moved
Production
X-Environment: production
Credentials: Live production keys
Money: Real payments processed
Quick Start
Choose your integration mode and follow the steps below. Both modes use the same API key.
πΊπ¬ Mode 1: Direct Charge β Uganda Mobile Money
No redirect. Customer stays on your page. You call the API β they approve on their phone.
Step 1: Get Your API Key
- Login to your LworxPay merchant dashboard
- Go to Merchant β API Configuration
- Copy your API Key
Step 2: Send Charge Request
- POST to
/api/v1/direct-charge - Pass phone number, amount, currency
- Customer gets MoMo prompt on phone
- Get back a
trx_idto track payment
Step 3: Track Payment
- Poll
/api/v1/charge-status/{trx_id}every 3 seconds - Or receive webhook on your
ipn_url - When status =
successβ fulfill order
Step 4: Verify & Fulfill
- Verify webhook signature using your Client Secret
- Check
status === "completed" - Fulfill order, send receipt, update DB
Minimal Example (cURL):
curl -X POST https://lworx.ug-web.com/api/v1/direct-charge \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "0700123456",
"amount": 5000,
"currency": "UGX",
"description": "Order #123",
"reference": "ORD-001",
"ipn_url": "https://yoursite.com/webhook/lworxpay"
}'Response:
{
"success": true,
"message": "Payment prompt sent to +256700123456. Customer should approve on their phone.",
"trx_id": "TXN1234567890",
"amount": 5000,
"currency": "UGX",
"status": "pending",
"status_url": "https://lworx.ug-web.com/api/v1/charge-status/TXN1234567890"
}π Mode 2: Payment Link β All Countries
Works worldwide via Flutterwave. You get a payment URL β redirect your customer to it. They pay using card, bank, or mobile money.
Step 1: Get Credentials
- Login to LworxPay merchant dashboard
- Go to Merchant β API Configuration
- Copy Merchant Key, API Key, and Client Secret
Step 2: Initiate Payment
- POST to
/api/v1/initiate-payment - Pass amount, currency, redirect URLs, ipn_url
- Receive a
payment_url - Redirect your customer to that URL
Step 3: Receive Webhook
- LworxPay POSTs to your
ipn_url - Verify
X-Signatureheader - Check
status === "completed" - Fulfill the order
Step 4: Go Live
- Test fully in sandbox mode
- Switch
X-Environmenttoproduction - Use production credentials
- Start accepting real payments
Minimal Example (cURL):
curl -X POST https://lworx.ug-web.com/api/v1/initiate-payment \
-H "Content-Type: application/json" \
-H "X-Environment: production" \
-H "X-Merchant-Key: YOUR_MERCHANT_KEY" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"payment_amount": 5000,
"currency_code": "UGX",
"ref_trx": "ORDER_12345",
"description": "Payment for Order #12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"ipn_url": "https://yoursite.com/webhook/lworxpay",
"success_redirect": "https://yoursite.com/payment/success",
"cancel_redirect": "https://yoursite.com/payment/cancelled"
}'Response:
{
"payment_url": "https://lworx.ug-web.com/payment/checkout?token=...",
"info": {
"ref_trx": "ORDER_12345",
"amount": 5000,
"currency_code": "UGX"
}
}πΊπ¬ Direct Charge API
Charge a Uganda mobile money number directly β no redirect, no hosted page. Customer gets a prompt on their phone and approves. Works with MTN MoMo and Airtel Money.
/api/v1/direct-charge
Authentication
| Header | Value | Required |
|---|---|---|
Authorization | Bearer YOUR_API_KEY | β |
Content-Type | application/json | β |
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | β | Uganda mobile number β MTN or Airtel. Formats accepted: 0700123456 or 256700123456 |
amount | number | β | Amount to charge. Minimum 500 UGX. |
currency | string | β | Currency code. Use UGX for Uganda Shillings. |
description | string | β | Payment description shown to customer |
reference | string | β | Your own order/transaction reference. Returned in webhook. |
ipn_url | string | β | Your webhook URL. LworxPay will POST to this when payment completes or fails. |
Example Request
curl -X POST https://lworx.ug-web.com/api/v1/direct-charge \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "0700123456",
"amount": 10000,
"currency": "UGX",
"description": "Payment for Order #456",
"reference": "ORD-456",
"ipn_url": "https://yoursite.com/webhook/lworxpay"
}'Success Response
{
"success": true,
"message": "Payment prompt sent to +256700123456. Customer should approve on their phone.",
"trx_id": "TXNABC123456789",
"reference": "ORD-456",
"amount": 10000,
"fee": 200,
"net_amount": 9800,
"currency": "UGX",
"status": "pending",
"status_url": "https://lworx.ug-web.com/api/v1/charge-status/TXNABC123456789"
}Error Response
{
"success": false,
"error": "Failed to initiate payment. Invalid phone number."
}Check Charge Status
Poll this endpoint every 3β5 seconds to check if the customer has approved the payment. Alternatively, receive the result via webhook β no polling needed.
/api/v1/charge-status/{trx_id}
Authentication
| Header | Value |
|---|---|
Authorization | Bearer YOUR_API_KEY |
Response β Pending
{
"success": true,
"trx_id": "TXNABC123456789",
"status": "pending",
"amount": 10000,
"currency": "UGX"
}Response β Success
{
"success": true,
"trx_id": "TXNABC123456789",
"status": "success",
"amount": 10000,
"fee": 200,
"net_amount": 9800,
"currency": "UGX",
"reference": "ORD-456",
"created_at": "2026-03-12T10:00:00.000000Z",
"updated_at": "2026-03-12T10:01:23.000000Z"
}Response β Failed
{
"success": true,
"trx_id": "TXNABC123456789",
"status": "failed",
"amount": 10000,
"currency": "UGX"
}Polling Example (JavaScript)
async function pollPaymentStatus(trxId, apiKey) {
const maxAttempts = 20; // ~60 seconds
let attempts = 0;
const poll = setInterval(async () => {
attempts++;
const res = await fetch(`/api/v1/charge-status/${trxId}`, {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await res.json();
if (data.status === 'success') {
clearInterval(poll);
// Payment confirmed β fulfill order
onPaymentSuccess(data);
} else if (data.status === 'failed') {
clearInterval(poll);
// Payment failed or expired
onPaymentFailed(data);
} else if (attempts >= maxAttempts) {
clearInterval(poll);
// Timed out waiting
onPaymentTimeout();
}
}, 3000); // poll every 3 seconds
}
function onPaymentSuccess(data) {
document.getElementById('status').textContent = 'β
Payment Confirmed!';
// fulfill order, update UI, etc.
}
π Initiate Payment β All Countries
Create a payment request and get a checkout URL to redirect your customer to. Works worldwide via Flutterwave β cards, bank transfer, and mobile money depending on the customer's country.
/api/v1/initiate-payment
Request Headers
| Header | Value | Required | Description |
|---|---|---|---|
Content-Type |
application/json |
β | Request content type |
X-Environment |
sandbox | production |
β | API environment |
X-Merchant-Key |
{merchant_key} |
β | Your Merchant ID from dashboard |
X-API-Key |
{api_key} |
β | Your API Key from dashboard |
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_amount | number | β | Amount to charge the customer |
currency_code | string | β | Currency code e.g. UGX, KES, NGN, USD, GBP |
ref_trx | string | β | Your unique order/transaction reference. Returned in webhook. |
description | string | β | Payment description shown on checkout page |
customer_name | string | β | Customer full name |
customer_email | string | β | Customer email address |
ipn_url | string | β | Your webhook URL β LworxPay posts payment result here |
success_redirect | string | β | URL to redirect customer to after successful payment |
cancel_redirect | string | β | URL to redirect customer to if payment is cancelled |
Example Request
curl -X POST https://lworx.ug-web.com/api/v1/initiate-payment \
-H "Content-Type: application/json" \
-H "X-Environment: production" \
-H "X-Merchant-Key: YOUR_MERCHANT_KEY" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"payment_amount": 50000,
"currency_code": "UGX",
"ref_trx": "ORDER_12345",
"description": "Payment for Order #12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"ipn_url": "https://yoursite.com/webhook/lworxpay",
"success_redirect": "https://yoursite.com/payment/success",
"cancel_redirect": "https://yoursite.com/payment/cancelled"
}'Success Response
{
"payment_url": "https://lworx.ug-web.com/payment/checkout?token=eyJ0eXAiOiJKV1Qi...",
"info": {
"ref_trx": "ORDER_12345",
"amount": 50000,
"currency_code": "UGX",
"description": "Payment for Order #12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"environment": "production"
}
}payment_url. They will see the LworxPay checkout page where they can select their payment method and complete the payment. Your webhook fires when they are done.
Verify Payment
Check the status of any payment by its transaction reference. Use this if you missed a webhook or want to confirm payment status manually.
/api/v1/verify-payment/{ref_trx}
Authentication
| Header | Value | Required |
|---|---|---|
X-Merchant-Key | {merchant_key} | β |
X-API-Key | {api_key} | β |
X-Environment | production | β |
Example Request
curl https://lworx.ug-web.com/api/v1/verify-payment/ORDER_12345 \
-H "X-Merchant-Key: YOUR_MERCHANT_KEY" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Environment: production"Response
{
"status": "completed",
"ref_trx": "ORDER_12345",
"amount": 50000,
"currency": "UGX",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"created_at": "2026-03-12T10:00:00.000000Z",
"completed_at": "2026-03-12T10:02:45.000000Z"
}Payment Status Values
| Status | Meaning | Action |
|---|---|---|
pending | Payment initiated, customer has not paid yet | Wait or poll again |
completed | Payment was successful | Fulfill the order |
failed | Payment failed or was declined | Notify customer to retry |
cancelled | Customer cancelled the payment | Redirect to cancel page |
Webhooks β Step by Step Guide
Webhooks are how LworxPay tells your server that a payment was completed, failed, or cancelled β automatically, without you needing to check. This works for both Direct Charge and Payment Link modes.
Why Webhooks?
When a customer pays, LworxPay sends a POST request to your ipn_url with the result. Your server verifies it and fulfills the order β fully automatic. No manual checking, no polling needed.
Step 1 β Set Your Webhook URL
Pass your webhook URL as ipn_url when initiating a payment or direct charge:
{
"ipn_url": "https://yoursite.com/webhook/lworxpay"
}- Must be a publicly accessible HTTPS URL
- Must return HTTP 200 within 10 seconds
- No authentication required β LworxPay sends a signature instead
Step 2 β Receive the Webhook Payload
LworxPay sends a POST request with JSON body to your URL:
For Direct Charge (Uganda):
{
"data": {
"ext_reference": "ORD-456",
"phone": "+256700123456",
"channel": "direct_charge",
"merchant_name": "Your Store"
},
"message": "Payment Completed",
"status": "completed",
"timestamp": 1710240000
}For Payment Link (All Countries):
{
"data": {
"ref_trx": "ORDER_12345",
"description": "Payment for Order #12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"merchant_name": "Your Store",
"amount": 50000,
"currency_code": "UGX",
"ipn_url": "https://yoursite.com/webhook/lworxpay",
"success_redirect": "https://yoursite.com/payment/success",
"cancel_redirect": "https://yoursite.com/payment/cancelled"
},
"message": "Payment Completed",
"status": "completed",
"timestamp": 1710240000
}Step 3 β Verify the Signature
Every webhook includes an X-Signature header. Always verify this before processing to prevent fake webhooks.
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 of the JSON body signed with your Client Secret |
Content-Type | application/json |
<?php
// routes/web.php β webhook endpoint
Route::post('/webhook/lworxpay', function (Request $request) {
// 1. Get raw body and signature
$payload = $request->getContent();
$signature = $request->header('X-Signature');
$secret = env('LWORXPAY_CLIENT_SECRET'); // your Client Secret
// 2. Verify signature
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
// 3. Parse payload
$data = json_decode($payload, true);
$status = $data['status']; // "completed", "failed"
// For Direct Charge
$reference = $data['data']['ext_reference'] ?? null; // your ORD-456
// For Payment Link
$refTrx = $data['data']['ref_trx'] ?? null; // your ORDER_12345
// 4. Handle payment
if ($status === 'completed') {
// Fulfill order using $reference or $refTrx
Order::where('ref', $reference ?? $refTrx)->update(['status' => 'paid']);
}
// 5. Always return 200
return response()->json(['status' => 'ok']);
});const express = require('express');
const crypto = require('crypto');
const app = express();
// Must use raw body for signature verification
app.use('/webhook/lworxpay', express.raw({ type: 'application/json' }));
app.post('/webhook/lworxpay', (req, res) => {
// 1. Get raw body and signature
const payload = req.body.toString();
const signature = req.headers['x-signature'];
const secret = process.env.LWORXPAY_CLIENT_SECRET;
// 2. Verify signature
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (expected !== signature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 3. Parse payload
const data = JSON.parse(payload);
const status = data.status;
const reference = data.data?.ext_reference || data.data?.ref_trx;
// 4. Handle payment
if (status === 'completed') {
// Fulfill order using reference
fulfillOrder(reference);
}
// 5. Always return 200
res.json({ status: 'ok' });
});
import hmac, hashlib, json, os
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook/lworxpay', methods=['POST'])
def lworxpay_webhook():
# 1. Get raw body and signature
payload = request.get_data()
signature = request.headers.get('X-Signature', '')
secret = os.environ.get('LWORXPAY_CLIENT_SECRET', '').encode()
# 2. Verify signature
expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return jsonify({'error': 'Invalid signature'}), 401
# 3. Parse payload
data = json.loads(payload)
status = data.get('status')
reference = data.get('data', {}).get('ext_reference') \
or data.get('data', {}).get('ref_trx')
# 4. Handle payment
if status == 'completed':
fulfill_order(reference)
# 5. Always return 200
return jsonify({'status': 'ok'})
Step 4 β Fulfill the Order
When status is completed, use the reference to identify and fulfill the order in your database:
<?php
if ($status === 'completed') {
$ref = $data['data']['ext_reference'] ?? $data['data']['ref_trx'];
$order = Order::where('reference', $ref)->firstOrFail();
if ($order->status !== 'paid') { // Idempotency check
$order->update(['status' => 'paid', 'paid_at' => now()]);
// Send receipt email, provision service, etc.
Mail::to($order->customer_email)->send(new OrderPaidMail($order));
}
}
Testing Webhooks During Development
Use webhook.site or ngrok to receive webhooks on your local machine:
# Install ngrok then expose your local server
ngrok http 8000
# Your webhook URL becomes something like:
# https://abc123.ngrok.io/webhook/lworxpay
# Pass this as ipn_url in your test requestsIntegration Examples
Complete examples for both Direct Charge (Uganda) and Payment Link (All Countries). Copy and adapt to your project.
<?php
// .env
// LWORXPAY_API_KEY=your_api_key
// LWORXPAY_CLIENT_SECRET=your_client_secret
// DirectPaymentController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class DirectPaymentController extends Controller
{
protected string $apiKey;
protected string $baseUrl = 'https://lworx.ug-web.com';
public function __construct()
{
$this->apiKey = env('LWORXPAY_API_KEY');
}
// Step 1: Initiate direct charge
public function charge(Request $request)
{
$request->validate([
'phone' => 'required|string',
'amount' => 'required|numeric|min:500',
]);
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
])->post("{$this->baseUrl}/api/v1/direct-charge", [
'phone' => $request->phone,
'amount' => $request->amount,
'currency' => 'UGX',
'description' => 'Order #' . $request->order_id,
'reference' => 'ORD-' . $request->order_id,
'ipn_url' => route('webhook.lworxpay'),
]);
if ($response->json('success')) {
return response()->json([
'trx_id' => $response->json('trx_id'),
'status_url' => $response->json('status_url'),
'message' => 'Approve the payment on your phone',
]);
}
return response()->json(['error' => $response->json('error')], 422);
}
// Step 2: Check status (called by your frontend every 3 seconds)
public function status(Request $request, string $trxId)
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->get("{$this->baseUrl}/api/v1/charge-status/{$trxId}");
return response()->json($response->json());
}
}
// routes/web.php
Route::post('/pay/direct', [DirectPaymentController::class, 'charge']);
Route::get('/pay/status/{trxId}', [DirectPaymentController::class, 'status']);
// Webhook handler
Route::post('/webhook/lworxpay', function (Request $request) {
$payload = $request->getContent();
$signature = $request->header('X-Signature');
$expected = hash_hmac('sha256', $payload, env('LWORXPAY_CLIENT_SECRET'));
if (!hash_equals($expected, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
$data = json_decode($payload, true);
if ($data['status'] === 'completed') {
$ref = $data['data']['ext_reference'];
$orderId = str_replace('ORD-', '', $ref);
Order::find($orderId)?->update(['status' => 'paid']);
}
return response()->json(['status' => 'ok']);
})->name('webhook.lworxpay');const axios = require('axios');
const crypto = require('crypto');
const express = require('express');
const app = express();
const API_KEY = process.env.LWORXPAY_API_KEY;
const CLIENT_SECRET = process.env.LWORXPAY_CLIENT_SECRET;
const BASE_URL = 'https://lworx.ug-web.com';
// Step 1: Initiate direct charge
app.post('/pay/direct', async (req, res) => {
const { phone, amount, orderId } = req.body;
const response = await axios.post(`${BASE_URL}/api/v1/direct-charge`, {
phone,
amount,
currency: 'UGX',
description: `Order #${orderId}`,
reference: `ORD-${orderId}`,
ipn_url: 'https://yoursite.com/webhook/lworxpay',
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
}
});
if (response.data.success) {
res.json({
trxId: response.data.trx_id,
statusUrl: response.data.status_url,
message: 'Approve the payment on your phone',
});
} else {
res.status(422).json({ error: response.data.error });
}
});
// Step 2: Check status
app.get('/pay/status/:trxId', async (req, res) => {
const { trxId } = req.params;
const response = await axios.get(`${BASE_URL}/api/v1/charge-status/${trxId}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
res.json(response.data);
});
// Webhook handler
app.use('/webhook/lworxpay', express.raw({ type: 'application/json' }));
app.post('/webhook/lworxpay', (req, res) => {
const payload = req.body.toString();
const signature = req.headers['x-signature'];
const expected = crypto.createHmac('sha256', CLIENT_SECRET).update(payload).digest('hex');
if (expected !== signature) return res.status(401).json({ error: 'Invalid signature' });
const data = JSON.parse(payload);
if (data.status === 'completed') {
const orderId = data.data.ext_reference.replace('ORD-', '');
// Update order in DB
db.orders.update({ id: orderId }, { status: 'paid' });
}
res.json({ status: 'ok' });
});
import requests, hmac, hashlib, os, json
from flask import Flask, request, jsonify
app = Flask(__name__)
API_KEY = os.environ.get('LWORXPAY_API_KEY')
SECRET = os.environ.get('LWORXPAY_CLIENT_SECRET')
BASE_URL = 'https://lworx.ug-web.com'
HEADERS = {'Authorization': f'Bearer {API_KEY}', 'Content-Type': 'application/json'}
# Step 1: Initiate direct charge
@app.route('/pay/direct', methods=['POST'])
def charge():
body = request.json
res = requests.post(f'{BASE_URL}/api/v1/direct-charge', json={
'phone': body['phone'],
'amount': body['amount'],
'currency': 'UGX',
'description': f"Order #{body['order_id']}",
'reference': f"ORD-{body['order_id']}",
'ipn_url': 'https://yoursite.com/webhook/lworxpay',
}, headers=HEADERS)
return jsonify(res.json())
# Step 2: Check status
@app.route('/pay/status/')
def status(trx_id):
res = requests.get(f'{BASE_URL}/api/v1/charge-status/{trx_id}', headers=HEADERS)
return jsonify(res.json())
# Webhook handler
@app.route('/webhook/lworxpay', methods=['POST'])
def webhook():
payload = request.get_data()
signature = request.headers.get('X-Signature', '')
expected = hmac.new(SECRET.encode(), payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return jsonify({'error': 'Invalid signature'}), 401
data = json.loads(payload)
status = data.get('status')
ref = data.get('data', {}).get('ext_reference', '')
if status == 'completed':
order_id = ref.replace('ORD-', '')
# Update order in DB
db.update_order(order_id, status='paid')
return jsonify({'status': 'ok'})
# Step 1: Charge the phone
curl -X POST https://lworx.ug-web.com/api/v1/direct-charge \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "0700123456",
"amount": 10000,
"currency": "UGX",
"description": "Order #123",
"reference": "ORD-123",
"ipn_url": "https://yoursite.com/webhook/lworxpay"
}'
# Step 2: Check status
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://lworx.ug-web.com/api/v1/charge-status/TXNABC123456<?php
// Initiate payment and redirect
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'X-Environment' => 'production',
'X-Merchant-Key' => env('LWORXPAY_MERCHANT_KEY'),
'X-API-Key' => env('LWORXPAY_API_KEY'),
])->post('https://lworx.ug-web.com/api/v1/initiate-payment', [
'payment_amount' => 50000,
'currency_code' => 'UGX',
'ref_trx' => 'ORDER_12345',
'description' => 'Payment for Order #12345',
'customer_name' => 'John Doe',
'customer_email' => 'john@example.com',
'ipn_url' => route('webhook.lworxpay'),
'success_redirect' => route('payment.success'),
'cancel_redirect' => route('payment.cancelled'),
]);
return redirect($response->json('payment_url'));const res = await axios.post('https://lworx.ug-web.com/api/v1/initiate-payment', {
payment_amount: 50000,
currency_code: 'UGX',
ref_trx: 'ORDER_12345',
description: 'Payment for Order #12345',
customer_name: 'John Doe',
customer_email: 'john@example.com',
ipn_url: 'https://yoursite.com/webhook/lworxpay',
success_redirect: 'https://yoursite.com/payment/success',
cancel_redirect: 'https://yoursite.com/payment/cancelled',
}, {
headers: {
'Content-Type': 'application/json',
'X-Environment': 'production',
'X-Merchant-Key': process.env.LWORXPAY_MERCHANT_KEY,
'X-API-Key': process.env.LWORXPAY_API_KEY,
}
});
// Redirect customer
res.redirect(res.data.payment_url);import requests
res = requests.post('https://lworx.ug-web.com/api/v1/initiate-payment', json={
'payment_amount': 50000,
'currency_code': 'UGX',
'ref_trx': 'ORDER_12345',
'description': 'Payment for Order #12345',
'customer_name': 'John Doe',
'customer_email': 'john@example.com',
'ipn_url': 'https://yoursite.com/webhook/lworxpay',
'success_redirect': 'https://yoursite.com/payment/success',
'cancel_redirect': 'https://yoursite.com/payment/cancelled',
}, headers={
'Content-Type': 'application/json',
'X-Environment': 'production',
'X-Merchant-Key': os.environ['LWORXPAY_MERCHANT_KEY'],
'X-API-Key': os.environ['LWORXPAY_API_KEY'],
})
payment_url = res.json()['payment_url']
return redirect(payment_url)curl -X POST https://lworx.ug-web.com/api/v1/initiate-payment \
-H "Content-Type: application/json" \
-H "X-Environment: production" \
-H "X-Merchant-Key: YOUR_MERCHANT_KEY" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"payment_amount": 50000,
"currency_code": "UGX",
"ref_trx": "ORDER_12345",
"description": "Payment for Order #12345",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"ipn_url": "https://yoursite.com/webhook/lworxpay",
"success_redirect": "https://yoursite.com/payment/success",
"cancel_redirect": "https://yoursite.com/payment/cancelled"
}'π Flutterwave Integration Guide
LworxPay uses Flutterwave as the payment processor for the Payment Link mode. This enables you to accept payments from customers in any country β cards, bank transfer, mobile money, and more.
Supported Payment Methods by Country
| Country / Region | Payment Methods | Currency |
|---|---|---|
| πΊπ¬ Uganda | MTN MoMo, Airtel Money, Visa/Mastercard | UGX |
| π°πͺ Kenya | M-Pesa, Visa/Mastercard, Bank | KES |
| π³π¬ Nigeria | Bank Transfer, Visa/Mastercard, USSD | NGN |
| π¬π Ghana | MTN MoMo, Vodafone Cash, Visa/Mastercard | GHS |
| πΉπΏ Tanzania | Vodacom M-Pesa, Airtel, Visa/Mastercard | TZS |
| π·πΌ Rwanda | MTN MoMo, Airtel, Visa/Mastercard | RWF |
| πΏπ¦ South Africa | Visa/Mastercard, Bank EFT | ZAR |
| π Rest of Africa & World | Visa/Mastercard | USD, EUR, GBP |
How It Works
Your server calls /api/v1/initiate-payment
You redirect customer to payment_url
Customer selects method & pays on LworxPay checkout
LworxPay fires webhook to your ipn_url
Full PHP Example
<?php
// ================================================================
// LworxPay Payment Link β All Countries (via Flutterwave)
// ================================================================
// .env
// LWORXPAY_BASE_URL=https://lworx.ug-web.com
// LWORXPAY_MERCHANT_KEY=your_merchant_key
// LWORXPAY_API_KEY=your_api_key
// LWORXPAY_CLIENT_SECRET=your_client_secret
class LworxPayService
{
protected string $baseUrl;
public function __construct()
{
$this->baseUrl = env('LWORXPAY_BASE_URL', 'https://lworx.ug-web.com');
}
public function initiatePayment(array $order): string
{
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'X-Environment' => 'production',
'X-Merchant-Key' => env('LWORXPAY_MERCHANT_KEY'),
'X-API-Key' => env('LWORXPAY_API_KEY'),
])->post($this->baseUrl . '/api/v1/initiate-payment', [
'payment_amount' => $order['amount'],
'currency_code' => $order['currency'] ?? 'UGX',
'ref_trx' => $order['reference'],
'description' => $order['description'],
'customer_name' => $order['customer_name'],
'customer_email' => $order['customer_email'],
'ipn_url' => route('webhook.lworxpay'),
'success_redirect' => route('payment.success'),
'cancel_redirect' => route('payment.cancelled'),
]);
if (!$response->successful()) {
throw new Exception('LworxPay error: ' . $response->body());
}
return $response->json('payment_url');
}
public function verifyWebhook(string $payload, string $signature): bool
{
$expected = hash_hmac('sha256', $payload, env('LWORXPAY_CLIENT_SECRET'));
return hash_equals($expected, $signature);
}
}
// In your controller
class CheckoutController extends Controller
{
public function checkout(Request $request, LworxPayService $lworxpay)
{
$order = Order::findOrFail($request->order_id);
$paymentUrl = $lworxpay->initiatePayment([
'amount' => $order->total,
'currency' => 'UGX',
'reference' => 'ORD-' . $order->id,
'description' => 'Payment for Order #' . $order->id,
'customer_name' => $request->user()->name,
'customer_email'=> $request->user()->email,
]);
return redirect($paymentUrl);
}
}
// Webhook handler
Route::post('/webhook/lworxpay', function (Request $request) use ($lworxpay) {
$payload = $request->getContent();
$signature = $request->header('X-Signature');
if (!$lworxpay->verifyWebhook($payload, $signature)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
$data = json_decode($payload, true);
$status = $data['status'];
$ref = $data['data']['ref_trx'];
if ($status === 'completed') {
$orderId = str_replace('ORD-', '', $ref);
Order::find($orderId)?->update(['status' => 'paid']);
}
return response()->json(['status' => 'ok']);
});Node.js / Express Example
const axios = require('axios');
const crypto = require('crypto');
const LWORXPAY_BASE = process.env.LWORXPAY_BASE_URL || 'https://lworx.ug-web.com';
const MERCHANT_KEY = process.env.LWORXPAY_MERCHANT_KEY;
const API_KEY = process.env.LWORXPAY_API_KEY;
const CLIENT_SECRET = process.env.LWORXPAY_CLIENT_SECRET;
// Initiate payment
async function initiatePayment(order) {
const res = await axios.post(`${LWORXPAY_BASE}/api/v1/initiate-payment`, {
payment_amount: order.amount,
currency_code: order.currency || 'UGX',
ref_trx: order.reference,
description: order.description,
customer_name: order.customerName,
customer_email: order.customerEmail,
ipn_url: 'https://yoursite.com/webhook/lworxpay',
success_redirect: 'https://yoursite.com/payment/success',
cancel_redirect: 'https://yoursite.com/payment/cancelled',
}, {
headers: {
'Content-Type': 'application/json',
'X-Environment': 'production',
'X-Merchant-Key': MERCHANT_KEY,
'X-API-Key': API_KEY,
}
});
return res.data.payment_url;
}
// Webhook handler
app.use('/webhook/lworxpay', express.raw({ type: 'application/json' }));
app.post('/webhook/lworxpay', (req, res) => {
const payload = req.body.toString();
const signature = req.headers['x-signature'];
const expected = crypto.createHmac('sha256', CLIENT_SECRET).update(payload).digest('hex');
if (expected !== signature) return res.status(401).json({ error: 'Invalid signature' });
const data = JSON.parse(payload);
if (data.status === 'completed') {
// Fulfill order
fulfillOrder(data.data.ref_trx);
}
res.json({ status: 'ok' });
});WooCommerce Integration
Complete guide to integrating LworxPay payment gateway with your WooCommerce store. Accept automatic mobile money payments directly on your WordPress site.
LworxPay for WooCommerce
Accept mobile money and other payments automatically on your WooCommerce store with the LworxPay gateway plugin.
Download Plugin
Ready-to-use WooCommerce payment gateway plugin for LworxPay. Download, install, and configure with your API keys.
Requirements
- WordPress 5.0+
- WooCommerce 4.0+
- PHP 7.4+
- SSL Certificate (HTTPS)
- LworxPay Merchant Account
Key Features
Everything you need for seamless automatic payment processing
Automatic Payments
Orders are automatically confirmed when payment is received via webhook
Mobile Money
Accept MTN Mobile Money and Airtel Money from Ugandan customers
Secure & Verified
HMAC-SHA256 signature verification on every payment notification
Installation Guide
Set up LworxPay on your WooCommerce store in 5 simple steps
Download Plugin
Download the LworxPay WooCommerce plugin ZIP file using the button above.
Upload to WordPress
Go to your WordPress admin β Plugins β Add New β Upload Plugin and select the downloaded ZIP file.
Activate Plugin
Click Activate Plugin to enable LworxPay in your WooCommerce store.
Configure Credentials
Go to WooCommerce β Settings β Payments β LworxPay and enter your Merchant Key, API Key, and Client Secret from your LworxPay dashboard.
Test & Go Live
Enable Test Mode first using your sandbox credentials. Process a test order. When confirmed working, switch to production credentials and disable Test Mode.
Plugin Settings Reference
What each setting in the LworxPay WooCommerce plugin does
Merchant Key & API Key
Your LworxPay credentials from Merchant β API Configuration in your dashboard
RequiredClient Secret
Used to verify webhook signatures. Keeps fake payment notifications out of your store
RequiredWebhook URL
Auto-configured by the plugin. LworxPay will POST payment results here to auto-confirm orders
Auto-SetPre-Launch Checklist
Tick all boxes before accepting real payments
Production Readiness
Need Help with WooCommerce Setup?
Our technical team can walk you through the integration process.
Sandbox Testing Guide
Test your integration safely with no real money. The sandbox environment mirrors production exactly β webhooks fire, status updates work, everything behaves like live.
Sandbox Environment
No real money is moved. Webhooks fire to your ipn_url just like production. Use this to build and validate your full integration before going live.
Step 1: Get Sandbox Credentials
- Log in to your LworxPay merchant dashboard
- Go to Merchant β API Configuration
- Enable Sandbox Mode
- Copy your
test_prefixed API Key, Merchant Key, and Client Secret
Step 2: Set Sandbox Headers
For Payment Link mode, add this header:
X-Environment: sandbox
X-Merchant-Key: test_your_merchant_key
X-API-Key: test_your_api_keyFor Direct Charge mode:
Authorization: Bearer test_your_api_keyTesting Direct Charge (Uganda)
# Sandbox direct charge test
curl -X POST https://lworx.ug-web.com/api/v1/direct-charge \
-H "Authorization: Bearer test_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "0700000000",
"amount": 1000,
"currency": "UGX",
"description": "Sandbox test",
"reference": "TEST-001",
"ipn_url": "https://webhook.site/your-test-url"
}'ipn_url during development. It gives you a free URL that shows every webhook payload in real time β perfect for verifying your webhook handler before going live.
Testing Payment Link (All Countries)
# Sandbox payment link test
curl -X POST https://lworx.ug-web.com/api/v1/initiate-payment \
-H "Content-Type: application/json" \
-H "X-Environment: sandbox" \
-H "X-Merchant-Key: test_YOUR_MERCHANT_KEY" \
-H "X-API-Key: test_YOUR_API_KEY" \
-d '{
"payment_amount": 5000,
"currency_code": "UGX",
"ref_trx": "TEST_ORDER_001",
"description": "Sandbox test payment",
"customer_name": "Test User",
"customer_email": "test@example.com",
"ipn_url": "https://webhook.site/your-test-url",
"success_redirect": "https://yoursite.com/payment/success",
"cancel_redirect": "https://yoursite.com/payment/cancelled"
}'Sandbox Checklist
- Payment initiates without errors
- Webhook fires to your ipn_url
- Webhook signature verifies correctly
- Your server returns HTTP 200 to webhook
- Order is fulfilled on status = completed
- Duplicate webhook does not double-fulfill (idempotency check)
- Failure/cancel case handled gracefully
test_ prefixes. Change X-Environment to production. That's it β you're live.
Interactive API Testing
Test LworxPay API endpoints directly from this page using your sandbox credentials. No code needed.
Sandbox Mode SANDBOX
Enter your sandbox credentials from the LworxPay merchant dashboard. No real money is processed.
Your Sandbox Credentials
/lworxpay/test-charge
Live Test
/api/v1/initiate-payment
/api/v1/charge-status/{trx_id}
Response:
Error Codes
LworxPay API uses standard HTTP response codes. Always handle errors gracefully in your integration.
HTTP Status Codes
| Code | Status | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 400 | Bad Request | Missing or invalid parameters. Check the error message for details. |
| 401 | Unauthorized | Invalid or missing API credentials. Check your API Key / Merchant Key. |
| 403 | Forbidden | Account suspended or insufficient permissions. |
| 404 | Not Found | Transaction ID not found or wrong endpoint URL. |
| 422 | Validation Error | Request data failed validation. See errors field in response. |
| 429 | Too Many Requests | Rate limit exceeded. Slow down your requests. |
| 500 | Server Error | Internal server error. Contact support if this persists. |
Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
Invalid API key |
Wrong or missing Authorization header | Ensure header is: Authorization: Bearer YOUR_API_KEY |
Invalid signature |
Webhook signature mismatch | Use raw request body (not parsed JSON) and your Client Secret |
Failed to initiate payment |
Invalid phone number or insufficient funds | Check phone format: 0700123456 or 256700123456 |
Insufficient wallet balance |
Merchant wallet too low to process | Top up merchant wallet and retry |
Transaction already processed |
Duplicate reference submitted | Use a unique ref_trx / reference per transaction |
Support
Need help integrating? Our team is available to assist you.