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.

Two Integration Modes Choose the mode that fits your use case. Direct Charge triggers a mobile money prompt silently via API β€” no page redirect. Payment Link works globally via Flutterwave with a hosted checkout page.
πŸ‡ΊπŸ‡¬ 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
See Direct Charge API β†’
🌍 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
See Payment Link API β†’
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/v1

Which Mode Should I Use?

Scenario Use
My customers are in Uganda and have MTN or AirtelDirect Charge
I want customers to pay without leaving my pageDirect Charge
My customers are outside Uganda or use cards/bankPayment Link
I want a hosted checkout page (no PCI burden)Payment Link
WooCommerce / e-commerce store globallyPayment 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/json

Credentials 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/json

Credentials needed: Merchant Key + API Key

Where to Find Your Credentials

  1. Log in to your LworxPay merchant dashboard
  2. Navigate to Merchant β†’ API Configuration
  3. Copy your Merchant ID, API Key, and Client Secret
  4. 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

Security Never expose your API Key or Client Secret in frontend JavaScript or mobile app source code. All API calls must be made from your server. The Client Secret is only used server-side to verify webhooks.

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
  1. Login to your LworxPay merchant dashboard
  2. Go to Merchant β†’ API Configuration
  3. Copy your API Key
Step 2: Send Charge Request
  1. POST to /api/v1/direct-charge
  2. Pass phone number, amount, currency
  3. Customer gets MoMo prompt on phone
  4. Get back a trx_id to track payment
Step 3: Track Payment
  1. Poll /api/v1/charge-status/{trx_id} every 3 seconds
  2. Or receive webhook on your ipn_url
  3. When status = success β†’ fulfill order
Step 4: Verify & Fulfill
  1. Verify webhook signature using your Client Secret
  2. Check status === "completed"
  3. 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
  1. Login to LworxPay merchant dashboard
  2. Go to Merchant β†’ API Configuration
  3. Copy Merchant Key, API Key, and Client Secret
Step 2: Initiate Payment
  1. POST to /api/v1/initiate-payment
  2. Pass amount, currency, redirect URLs, ipn_url
  3. Receive a payment_url
  4. Redirect your customer to that URL
Step 3: Receive Webhook
  1. LworxPay POSTs to your ipn_url
  2. Verify X-Signature header
  3. Check status === "completed"
  4. Fulfill the order
Step 4: Go Live
  1. Test fully in sandbox mode
  2. Switch X-Environment to production
  3. Use production credentials
  4. 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.

Best for Uganda If your customers are in Uganda and pay via MTN or Airtel, this is the simplest integration. One API call β€” done. Customer never leaves your page.
POST /api/v1/direct-charge

Authentication

HeaderValueRequired
AuthorizationBearer YOUR_API_KEYβœ…
Content-Typeapplication/jsonβœ…

Request Parameters

ParameterTypeRequiredDescription
phonestringβœ…Uganda mobile number β€” MTN or Airtel. Formats accepted: 0700123456 or 256700123456
amountnumberβœ…Amount to charge. Minimum 500 UGX.
currencystringβœ…Currency code. Use UGX for Uganda Shillings.
descriptionstring❌Payment description shown to customer
referencestring❌Your own order/transaction reference. Returned in webhook.
ipn_urlstring❌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."
}
What Happens After? The customer receives a mobile money prompt on their phone immediately. They have approximately 60 seconds to enter their PIN and approve. Your webhook fires when they approve or the request expires.

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.

GET /api/v1/charge-status/{trx_id}

Authentication

HeaderValue
AuthorizationBearer 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.

Uganda users? If your customers are in Uganda and use MTN or Airtel, consider using the Direct Charge API instead β€” no redirect, simpler integration.
POST /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_amountnumberβœ…Amount to charge the customer
currency_codestringβœ…Currency code e.g. UGX, KES, NGN, USD, GBP
ref_trxstringβœ…Your unique order/transaction reference. Returned in webhook.
descriptionstringβœ…Payment description shown on checkout page
customer_namestringβœ…Customer full name
customer_emailstringβœ…Customer email address
ipn_urlstringβœ…Your webhook URL β€” LworxPay posts payment result here
success_redirectstringβœ…URL to redirect customer to after successful payment
cancel_redirectstringβœ…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"
  }
}
Next Step Redirect your customer to the 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.

GET /api/v1/verify-payment/{ref_trx}

Authentication

HeaderValueRequired
X-Merchant-Key{merchant_key}βœ…
X-API-Key{api_key}βœ…
X-Environmentproductionβœ…

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

StatusMeaningAction
pendingPayment initiated, customer has not paid yetWait or poll again
completedPayment was successfulFulfill the order
failedPayment failed or was declinedNotify customer to retry
cancelledCustomer cancelled the paymentRedirect 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"
}
Requirements
  • 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.

HeaderDescription
X-SignatureHMAC-SHA256 of the JSON body signed with your Client Secret
Content-Typeapplication/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));
    }
}
Retry Logic LworxPay retries failed webhooks up to 5 times with exponential backoff. Always return HTTP 200 immediately after verifying. Do heavy processing in a background job to avoid timeouts.

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 requests

Integration Examples

Complete examples for both Direct Charge (Uganda) and Payment Link (All Countries). Copy and adapt to your project.

Direct Charge No redirect. Customer stays on your page. Works for Uganda MTN & Airtel only.
<?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

🌍 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.

No Flutterwave Account Needed You do not need your own Flutterwave account. LworxPay handles all Flutterwave integration. You just use the LworxPay API and we process via Flutterwave on your behalf.

Supported Payment Methods by Country

Country / Region Payment Methods Currency
πŸ‡ΊπŸ‡¬ UgandaMTN MoMo, Airtel Money, Visa/MastercardUGX
πŸ‡°πŸ‡ͺ KenyaM-Pesa, Visa/Mastercard, BankKES
πŸ‡³πŸ‡¬ NigeriaBank Transfer, Visa/Mastercard, USSDNGN
πŸ‡¬πŸ‡­ GhanaMTN MoMo, Vodafone Cash, Visa/MastercardGHS
πŸ‡ΉπŸ‡Ώ TanzaniaVodacom M-Pesa, Airtel, Visa/MastercardTZS
πŸ‡·πŸ‡Ό RwandaMTN MoMo, Airtel, Visa/MastercardRWF
πŸ‡ΏπŸ‡¦ South AfricaVisa/Mastercard, Bank EFTZAR
🌍 Rest of Africa & WorldVisa/MastercardUSD, EUR, GBP

How It Works

1
Your server calls /api/v1/initiate-payment
2
You redirect customer to payment_url
3
Customer selects method & pays on LworxPay checkout
4
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' });
});
Currency Must Match The currency_code you send must match your merchant account currency. If your merchant account is set to UGX, you must send UGX. Contact support to enable multiple currencies.

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.

5Min Setup
99.9%Uptime
24/7Support
Download Plugin

Ready-to-use WooCommerce payment gateway plugin for LworxPay. Download, install, and configure with your API keys.

Version: 1.0
Secure & Verified
Download LworxPay Plugin ZIP
Requirements
  • WordPress 5.0+
  • WooCommerce 4.0+
  • PHP 7.4+
  • SSL Certificate (HTTPS)
  • LworxPay Merchant Account
Fully Compatible

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

1
Download Plugin

Download the LworxPay WooCommerce plugin ZIP file using the button above.

2
Upload to WordPress

Go to your WordPress admin β†’ Plugins β†’ Add New β†’ Upload Plugin and select the downloaded ZIP file.

3
Activate Plugin

Click Activate Plugin to enable LworxPay in your WooCommerce store.

4
Configure Credentials

Go to WooCommerce β†’ Settings β†’ Payments β†’ LworxPay and enter your Merchant Key, API Key, and Client Secret from your LworxPay dashboard.

5
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

Required
Client Secret

Used to verify webhook signatures. Keeps fake payment notifications out of your store

Required
Webhook URL

Auto-configured by the plugin. LworxPay will POST payment results here to auto-confirm orders

Auto-Set

Pre-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_key

For Direct Charge mode:

Authorization: Bearer test_your_api_key

Testing 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"
  }'
Webhook Testing Tip Use webhook.site as your 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
Going Live Once all checklist items pass in sandbox, switch to production credentials and remove the 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
POST /lworxpay/test-charge Live Test
This submits a real charge request. Customer gets a MoMo prompt on their phone and the spinner page opens automatically β€” same flow as the dashboard deposit.
GET/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

CodeStatusDescription
200OKRequest succeeded
400Bad RequestMissing or invalid parameters. Check the error message for details.
401UnauthorizedInvalid or missing API credentials. Check your API Key / Merchant Key.
403ForbiddenAccount suspended or insufficient permissions.
404Not FoundTransaction ID not found or wrong endpoint URL.
422Validation ErrorRequest data failed validation. See errors field in response.
429Too Many RequestsRate limit exceeded. Slow down your requests.
500Server ErrorInternal server error. Contact support if this persists.

Common Errors & Fixes

ErrorCauseFix
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.

Email Support

Technical integration help

support@lworx-pay.com
WhatsApp

Faster response β€” integration support

+256 766 637 669
Merchant Dashboard

Manage API keys, view transactions

Open Dashboard
Support Hours Monday – Saturday: 8:00 AM – 8:00 PM EAT. Emergency technical support available 24/7 via WhatsApp.