Accept and send mobile money from your own website or app.
One REST API, JSON in and JSON out. Every request is authenticated with your API key.
https://www.lworx-pay.com/api/v1Send your live API key in the Authorization header of every request. You will find it under API Keys in your dashboard (KYC approval is required to get one). Keep it secret and only use it from your server.
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json| Rate limit | 60 requests per minute per API key (then HTTP 429). |
| Phone numbers | MTN or Airtel, as 0770000000 or 256770000000. |
| Currency | UGX. Send amounts as plain numbers, e.g. 5000. |
| Reference | Your own ID. It must be unique per account on /collect, /collect-stanbic, /payout and /buy-airtime. |
| Timestamps | UTC. |
Every response has a success flag. Check it first, then read data (success) or error (failure).
{
"success": true,
"message": "Human readable note (when there is one)",
"data": {
"...": "endpoint specific fields"
}
}{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Enter a valid 10-digit phone number starting with 07."
}
}error.code (stable). The message text is for humans and may change. The HTTP status tells you the class of problem: 400 bad request, 401 bad key, 403 not allowed, 404 not found, 429 too many requests.success: true on /collect, /collect-stanbic, /payout and bill endpoints means the request was accepted, not that the money has moved. Wait for the webhook or call /status for the final result.| Status | Meaning |
|---|---|
| pending | Accepted and waiting (for example the customer has not approved the prompt yet). |
| completed | Finished successfully. |
| failed | Did not go through. Any money taken from your wallet is refunded automatically. |
| cancelled | Cancelled by the customer or the network. |
| refunded | Money was returned to your wallet. |
| pending_approval | Payouts only: waiting for your manual approval (when enabled in API settings). |
/collect Collect a paymentSend a mobile money prompt to your customer's phone. The customer approves with their PIN and the money is credited to your wallet.
status: "pending". The final result arrives on your webhook (payment.success / payment.failed) or by calling /status.| Name | Type | Description | |
|---|---|---|---|
amount | number | required | Amount in UGX you want to receive. Minimum 500. |
phone | string | required | Customer's MTN or Airtel number, e.g. 0770000000 or 256770000000. |
reference | string | required | Your own unique order ID. Reusing a reference returns DUPLICATE_REFERENCE. |
callback_url | string | optional | URL we POST the final result to. Separate several URLs with commas. |
webhook_secret | string | optional | Secret used to sign the callback (X-LworxPay-Signature header). |
curl -X POST "https://www.lworx-pay.com/api/v1/collect" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":5000,"phone":"0770000000","reference":"ORDER-123","callback_url":"https://yoursite.com/webhook"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/collect');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 5000,
'phone' => '0770000000',
'reference' => 'ORDER-123',
'callback_url' => 'https://yoursite.com/webhook',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/collect", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"amount": 5000,
"phone": "0770000000",
"reference": "ORDER-123",
"callback_url": "https://yoursite.com/webhook"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/collect",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"amount": 5000,
"phone": "0770000000",
"reference": "ORDER-123",
"callback_url": "https://yoursite.com/webhook"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Approve the prompt on your phone to pay UGX 5,200.",
"data": {
"transaction_id": "LWAK3F9QZ7A1C",
"reference": "ORDER-123",
"status": "pending",
"amount": 5000,
"charge_amount": 5200,
"phone": "0770000000"
}
}transaction_id | Our unique ID for this transaction. |
reference | The reference you sent. |
status | Always pending at this stage. |
amount | Amount credited to your wallet once paid. |
charge_amount | Total the customer is charged (amount + 4%). |
phone | Customer number in 07XXXXXXXX format. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "DUPLICATE_REFERENCE",
"message": "This reference has already been used"
}
}{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Enter a valid 10-digit phone number starting with 07."
}
}{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "Minimum amount is UGX 500"
}
}{
"success": false,
"error": {
"code": "PAYMENT_FAILED",
"message": "Payment processor rejected the request"
}
}{
"success": false,
"error": {
"code": "DEPOSIT_DISABLED",
"message": "Deposits are currently disabled via your API settings. Enable them in your dashboard."
}
}{
"success": false,
"error": {
"code": "DEPOSITS_BLOCKED",
"message": "Deposits are temporarily disabled platform-wide. Please try again later."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/collect-stanbic Collect a payment (Stanbic gateway, no fees)Send a mobile money prompt to your customer's phone through our Stanbic gateway. Works exactly like /collect, but with no charges at all.
amount you send (no 4% added) and your wallet is credited the full amount.status: "pending". The final result arrives on your webhook (payment.success / payment.failed) or by calling /status. A customer who cancels the prompt is reported as failed.| Name | Type | Description | |
|---|---|---|---|
amount | number | required | Amount in UGX. The customer pays exactly this. Minimum 500. |
phone | string | required | Customer's MTN or Airtel number, e.g. 0772000000 or 256772000000. |
reference | string | required | Your own unique order ID. Reusing a reference returns DUPLICATE_REFERENCE. |
callback_url | string | optional | URL we POST the final result to. Separate several URLs with commas. |
webhook_secret | string | optional | Secret used to sign the callback (X-LworxPay-Signature header). |
curl -X POST "https://www.lworx-pay.com/api/v1/collect-stanbic" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":5000,"phone":"0772000000","reference":"ORDER-124","callback_url":"https://yoursite.com/webhook"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/collect-stanbic');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 5000,
'phone' => '0772000000',
'reference' => 'ORDER-124',
'callback_url' => 'https://yoursite.com/webhook',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/collect-stanbic", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"amount": 5000,
"phone": "0772000000",
"reference": "ORDER-124",
"callback_url": "https://yoursite.com/webhook"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/collect-stanbic",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"amount": 5000,
"phone": "0772000000",
"reference": "ORDER-124",
"callback_url": "https://yoursite.com/webhook"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Approve the prompt on your phone to pay UGX 5,000.",
"data": {
"transaction_id": "LWSK3F9QZ7A1C",
"reference": "ORDER-124",
"status": "pending",
"amount": 5000,
"charge_amount": 5000,
"phone": "0772000000"
}
}transaction_id | Our unique ID for this transaction. |
reference | The reference you sent. |
status | Always pending at this stage. |
amount | Amount credited to your wallet once paid. |
charge_amount | Total the customer is charged. Always equal to amount (no fees). |
phone | Customer number in 07XXXXXXXX format. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "DUPLICATE_REFERENCE",
"message": "This reference has already been used"
}
}{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Enter a valid 10-digit phone number starting with 07."
}
}{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "Minimum amount is UGX 500"
}
}{
"success": false,
"error": {
"code": "PAYMENT_FAILED",
"message": "Payment processor rejected the request"
}
}{
"success": false,
"error": {
"code": "DEPOSIT_DISABLED",
"message": "Deposits are currently disabled via your API settings. Enable them in your dashboard."
}
}{
"success": false,
"error": {
"code": "DEPOSITS_BLOCKED",
"message": "Deposits are temporarily disabled platform-wide. Please try again later."
}
}{
"success": false,
"error": {
"code": "GATEWAY_UNAVAILABLE",
"message": "This payment method is temporarily unavailable. Please try again later."
}
}{
"success": false,
"error": {
"code": "GATEWAY_TIMEOUT",
"message": "We could not confirm this request. Check /status with your reference before trying again."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/payout Send a payoutSend money from your wallet to any MTN or Airtel number. KYC approval is required and new accounts must wait 24 hours before their first payout.
/withdraw.| Name | Type | Description | |
|---|---|---|---|
amount | number | required | Amount in UGX to send. Use 1,000 or more. |
phone | string | required | Recipient MTN or Airtel number. |
reference | string | required | Your own unique payout ID. |
callback_url | string | optional | URL we POST the final result to. |
curl -X POST "https://www.lworx-pay.com/api/v1/payout" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":10000,"phone":"0770000000","reference":"PAYOUT-1","callback_url":"https://yoursite.com/webhook"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/payout');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 10000,
'phone' => '0770000000',
'reference' => 'PAYOUT-1',
'callback_url' => 'https://yoursite.com/webhook',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/payout", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"amount": 10000,
"phone": "0770000000",
"reference": "PAYOUT-1",
"callback_url": "https://yoursite.com/webhook"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/payout",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"amount": 10000,
"phone": "0770000000",
"reference": "PAYOUT-1",
"callback_url": "https://yoursite.com/webhook"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Withdrawal sent. It will arrive shortly.",
"data": {
"transaction_id": "LWXM8T2PX4B7D",
"reference": "PAYOUT-1",
"status": "pending",
"amount": 10000,
"fee": 400
}
}{
"success": true,
"data": {
"transaction_id": "LWXM8T2PX4B7D",
"reference": "PAYOUT-1",
"status": "pending_approval",
"amount": 10000,
"fee": 400
}
}transaction_id | Our unique ID for this payout. |
status | pending, or pending_approval if you turned on manual approval in API settings. |
amount | Amount sent to the recipient. |
fee | Fee taken from your wallet on top of the amount. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "DUPLICATE_REFERENCE",
"message": "This reference has already been used"
}
}{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Enter a valid 10-digit phone number starting with 07."
}
}{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "Minimum withdrawal is UGX 500"
}
}{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. You need UGX 10,400 (10,000 + 400 fee) but have UGX 2,000."
}
}{
"success": false,
"error": {
"code": "PAYOUT_FAILED",
"message": "Withdrawal could not be sent. Your wallet has been refunded — please try again later."
}
}{
"success": false,
"error": {
"code": "KYC_REQUIRED",
"message": "KYC verification required. Complete identity verification before withdrawing."
}
}{
"success": false,
"error": {
"code": "ACCOUNT_COOLDOWN",
"message": "New accounts must wait 24 hours before withdrawing. 5 hour(s) remaining."
}
}{
"success": false,
"error": {
"code": "WITHDRAWAL_DISABLED",
"message": "Withdrawals are currently disabled via your API settings."
}
}{
"success": false,
"error": {
"code": "WITHDRAWALS_BLOCKED",
"message": "Withdrawals are temporarily disabled. Please try again later."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/status Check transaction statusLook up any transaction by your own reference or by our transaction_id.
| Name | Type | Description | |
|---|---|---|---|
reference | string | required | Your reference or our transaction_id (query string). ?transaction_id= also works. |
curl "https://www.lworx-pay.com/api/v1/status?reference=ORDER-123" \
-H "Authorization: Bearer YOUR_API_KEY"<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/status?reference=ORDER-123');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/status?reference=ORDER-123", {
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.get(
"https://www.lworx-pay.com/api/v1/status?reference=ORDER-123",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"data": {
"transaction_id": "LWAK3F9QZ7A1C",
"reference": "ORDER-123",
"status": "completed",
"amount": 5000,
"fee": 0,
"type": "deposit",
"phone": "0770000000",
"provider": "mtn",
"created_at": "2026-10-02 08:15:00",
"updated_at": "2026-10-02 08:16:12"
}
}{
"success": true,
"data": {
"transaction_id": "LWAK3F9QZ7A1C",
"reference": "ORDER-123",
"status": "pending",
"amount": 5000,
"fee": 0,
"type": "deposit",
"phone": "0770000000",
"provider": "mtn",
"created_at": "2026-10-02 08:15:00",
"updated_at": "2026-10-02 08:15:00"
}
}status | pending, completed, failed, cancelled or refunded. See statuses. |
type | deposit, withdrawal, airtime, bundle, electricity, water, tv. |
provider | Customer network: mtn or airtel. |
fee | Fee charged to your wallet (0 for collections). |
created_at / updated_at | UTC time, format YYYY-MM-DD HH:MM:SS. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Provide ?reference="
}
}{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Transaction not found"
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/balance Get wallet balanceReturns the current balance of your LWORXPAY wallet.
curl "https://www.lworx-pay.com/api/v1/balance" \
-H "Authorization: Bearer YOUR_API_KEY"<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/balance');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/balance", {
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.get(
"https://www.lworx-pay.com/api/v1/balance",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"data": {
"balance": 125000,
"currency": "UGX",
"merchant_id": "LWORX123456"
}
}balance | Wallet balance in UGX. |
currency | Always UGX. |
merchant_id | Your LWORXPAY merchant ID. |
This endpoint has no endpoint-specific errors.
Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/validate-number Validate a phone numberGet the name a mobile money number is registered under — useful to confirm the recipient before a payout.
ALICE NAKATO. If the network has no name on record, customer_name is "Unknown". Alias: /verify-number.| Name | Type | Description | |
|---|---|---|---|
phone | string | required | MTN or Airtel number to check. |
curl -X POST "https://www.lworx-pay.com/api/v1/validate-number" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"0770000000"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/validate-number');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phone' => '0770000000',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/validate-number", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"phone": "0770000000"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/validate-number",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"phone": "0770000000"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"data": {
"customer_name": "ALICE NAKATO",
"phone": "0770000000"
}
}customer_name | Registered name, UPPERCASE, or "Unknown". |
phone | The number exactly as you sent it. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required field: phone"
}
}{
"success": false,
"error": {
"code": "VERIFICATION_FAILED",
"message": "Could not verify this number"
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/buy-airtime Buy airtimeTop up airtime on any MTN or Airtel number from your wallet. No fee. KYC approval required.
| Name | Type | Description | |
|---|---|---|---|
amount | number | required | Airtime amount in UGX (500 – 5,000,000). |
phone | string | required | Number to top up. |
reference | string | required | Your own unique ID. |
callback_url | string | optional | URL we POST the final result to. |
curl -X POST "https://www.lworx-pay.com/api/v1/buy-airtime" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":1000,"phone":"0770000000","reference":"AIR-1"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/buy-airtime');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 1000,
'phone' => '0770000000',
'reference' => 'AIR-1',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/buy-airtime", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"amount": 1000,
"phone": "0770000000",
"reference": "AIR-1"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/buy-airtime",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"amount": 1000,
"phone": "0770000000",
"reference": "AIR-1"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Airtime purchase submitted.",
"data": {
"transaction_id": "LWXP2Q7R9S1TA",
"reference": "AIR-1",
"status": "completed",
"amount": 1000,
"fee": 0
}
}{
"success": true,
"message": "Airtime purchase submitted.",
"data": {
"transaction_id": "LWXP2Q7R9S1TA",
"reference": "AIR-1",
"status": "pending",
"amount": 1000,
"fee": 0
}
}status | completed when delivered instantly, otherwise pending (final result by webhook). |
amount | Airtime value in UGX. |
fee | Always 0 for airtime. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "DUPLICATE_REFERENCE",
"message": "This reference has already been used"
}
}{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Enter a valid 10-digit phone number starting with 07."
}
}{
"success": false,
"error": {
"code": "KYC_REQUIRED",
"message": "Approved KYC is required for bills and airtime."
}
}{
"success": false,
"error": {
"code": "LIMIT",
"message": "Maximum is UGX 5,000,000 per transaction."
}
}{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "Minimum airtime is UGX 500"
}
}{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. You need UGX 1,000 but have UGX 200."
}
}{
"success": false,
"error": {
"code": "PURCHASE_FAILED",
"message": "Purchase failed. Your wallet has been refunded."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/products List data & voice bundlesReturns the bundle catalogue. Use a bundle ID from this list when calling /buy-data.
/list-products.curl "https://www.lworx-pay.com/api/v1/products" \
-H "Authorization: Bearer YOUR_API_KEY"<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/products');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/products", {
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.get(
"https://www.lworx-pay.com/api/v1/products",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"data": {
"country": "UG",
"currency": "UGX",
"networks": [
{
"code": "MTN",
"airtime": true,
"bundles": true,
"min_airtime_amount": 500
},
{
"code": "AIRTEL",
"airtime": true,
"bundles": true,
"min_airtime_amount": 500
}
]
}
}networks[].code | MTN or AIRTEL. |
bundle product_id / bundle_id | The ID you pass as bundle_id in /buy-data. |
price | Bundle price in UGX (taken from your wallet). |
This endpoint has no endpoint-specific errors.
Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/buy-data Buy a data or voice bundleBuy a bundle from your wallet. The price is looked up from the catalogue automatically. No fee. KYC approval required.
/buy-voice.| Name | Type | Description | |
|---|---|---|---|
bundle_id | string | required | Bundle ID taken from /products. |
phone | string | required | Number that receives the bundle. |
reference | string | required | Your own unique ID. |
callback_url | string | optional | URL we POST the final result to. |
curl -X POST "https://www.lworx-pay.com/api/v1/buy-data" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"bundle_id":"ID_FROM_PRODUCTS","phone":"0770000000","reference":"DATA-1"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/buy-data');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'bundle_id' => 'ID_FROM_PRODUCTS',
'phone' => '0770000000',
'reference' => 'DATA-1',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/buy-data", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"bundle_id": "ID_FROM_PRODUCTS",
"phone": "0770000000",
"reference": "DATA-1"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/buy-data",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"bundle_id": "ID_FROM_PRODUCTS",
"phone": "0770000000",
"reference": "DATA-1"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Bundle purchase submitted.",
"data": {
"transaction_id": "LWXD4F6G8H2JK",
"reference": "DATA-1",
"status": "completed",
"amount": 5000,
"fee": 0
}
}status | completed or pending. |
amount | Bundle price taken from your wallet. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: phone, bundle_id, reference"
}
}{
"success": false,
"error": {
"code": "INVALID_BUNDLE",
"message": "Unknown bundle_id"
}
}{
"success": false,
"error": {
"code": "INVALID_PHONE",
"message": "Enter a valid 10-digit phone number starting with 07."
}
}{
"success": false,
"error": {
"code": "KYC_REQUIRED",
"message": "Approved KYC is required for bills and airtime."
}
}{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. You need UGX 5,000 but have UGX 1,200."
}
}{
"success": false,
"error": {
"code": "PURCHASE_FAILED",
"message": "Purchase failed. Your wallet has been refunded."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/buy-electricity Pay UMEME electricity (Yaka)Pay a prepaid UMEME meter from your wallet. Fee: UGX 500 per payment. KYC approval required.
| Name | Type | Description | |
|---|---|---|---|
meter_number | string | required | UMEME prepaid meter number. |
amount | number | required | Amount in UGX (up to 5,000,000). |
phone | string | required | Customer phone number. |
reference | string | required | Your own unique ID. |
customer_name | string | optional | Meter owner name, if you have it. |
curl -X POST "https://www.lworx-pay.com/api/v1/buy-electricity" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"meter_number":"04xxxxxxxxx","amount":20000,"phone":"0770000000","reference":"BILL-1"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/buy-electricity');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'meter_number' => '04xxxxxxxxx',
'amount' => 20000,
'phone' => '0770000000',
'reference' => 'BILL-1',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/buy-electricity", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"meter_number": "04xxxxxxxxx",
"amount": 20000,
"phone": "0770000000",
"reference": "BILL-1"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/buy-electricity",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"meter_number": "04xxxxxxxxx",
"amount": 20000,
"phone": "0770000000",
"reference": "BILL-1"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Electricity purchase submitted.",
"data": {
"transaction_id": "LWXB3C5D7E9FG",
"reference": "BILL-1",
"status": "pending",
"amount": 20000,
"fee": 500
}
}status | pending or completed. Final result by webhook (bill.success / bill.failed). |
amount | Amount paid to the meter. |
fee | Always 500 for bill payments. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: meter_number, amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "KYC_REQUIRED",
"message": "Approved KYC is required for bills and airtime."
}
}{
"success": false,
"error": {
"code": "LIMIT",
"message": "Maximum is UGX 5,000,000 per transaction."
}
}{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. You need UGX 20,500 but have UGX 3,000."
}
}{
"success": false,
"error": {
"code": "PURCHASE_FAILED",
"message": "Purchase failed. Your wallet has been refunded."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/buy-water Pay NWSC water billPay a National Water & Sewerage Corporation account from your wallet. Fee: UGX 500 per payment. KYC approval required.
| Name | Type | Description | |
|---|---|---|---|
meter_number | string | required | NWSC customer / meter number. |
area | string | required | NWSC service area of the customer. |
amount | number | required | Amount in UGX. |
phone | string | required | Customer phone number. |
reference | string | required | Your own unique ID. |
customer_name | string | optional | Account holder name, if you have it. |
curl -X POST "https://www.lworx-pay.com/api/v1/buy-water" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"meter_number":"1234567","area":"KAMPALA","amount":30000,"phone":"0770000000","reference":"WATER-1"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/buy-water');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'meter_number' => '1234567',
'area' => 'KAMPALA',
'amount' => 30000,
'phone' => '0770000000',
'reference' => 'WATER-1',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/buy-water", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"meter_number": "1234567",
"area": "KAMPALA",
"amount": 30000,
"phone": "0770000000",
"reference": "WATER-1"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/buy-water",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"meter_number": "1234567",
"area": "KAMPALA",
"amount": 30000,
"phone": "0770000000",
"reference": "WATER-1"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Water purchase submitted.",
"data": {
"transaction_id": "LWXW5X7Y9Z1AB",
"reference": "WATER-1",
"status": "pending",
"amount": 30000,
"fee": 500
}
}status | pending or completed. |
fee | Always 500. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: meter_number, amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "KYC_REQUIRED",
"message": "Approved KYC is required for bills and airtime."
}
}{
"success": false,
"error": {
"code": "LIMIT",
"message": "Maximum is UGX 5,000,000 per transaction."
}
}{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. You need UGX 30,500 but have UGX 3,000."
}
}{
"success": false,
"error": {
"code": "PURCHASE_FAILED",
"message": "Purchase failed. Your wallet has been refunded."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
/buy-tv Pay DStv / GOtv subscriptionRenew a DStv or GOtv subscription from your wallet. Fee: UGX 500 per payment. KYC approval required.
| Name | Type | Description | |
|---|---|---|---|
provider | string | required | DSTV or GOTV. |
bouquet_code | string | required | Package code, e.g. GOTVPLS. |
meter_number | string | required | Smartcard / IUC number. |
amount | number | required | Amount in UGX. |
phone | string | required | Customer phone number. |
reference | string | required | Your own unique ID. |
curl -X POST "https://www.lworx-pay.com/api/v1/buy-tv" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"GOTV","bouquet_code":"GOTVPLS","meter_number":"7012345678","amount":38000,"phone":"0770000000","reference":"TV-1"}'<?php
$ch = curl_init('https://www.lworx-pay.com/api/v1/buy-tv');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'provider' => 'GOTV',
'bouquet_code' => 'GOTVPLS',
'meter_number' => '7012345678',
'amount' => 38000,
'phone' => '0770000000',
'reference' => 'TV-1',
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result['success']) {
$data = $result['data'];
} else {
// $result['error']['code'] and $result['error']['message']
}const response = await fetch("https://www.lworx-pay.com/api/v1/buy-tv", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"provider": "GOTV",
"bouquet_code": "GOTVPLS",
"meter_number": "7012345678",
"amount": 38000,
"phone": "0770000000",
"reference": "TV-1"
}),
});
const result = await response.json();
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.code, result.error.message);
}import requests
response = requests.post(
"https://www.lworx-pay.com/api/v1/buy-tv",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"provider": "GOTV",
"bouquet_code": "GOTVPLS",
"meter_number": "7012345678",
"amount": 38000,
"phone": "0770000000",
"reference": "TV-1"
},
)
result = response.json()
if result["success"]:
print(result["data"])
else:
print(result["error"]["code"], result["error"]["message"]){
"success": true,
"message": "Tv purchase submitted.",
"data": {
"transaction_id": "LWXT6U8V1W3XY",
"reference": "TV-1",
"status": "pending",
"amount": 38000,
"fee": 500
}
}status | pending or completed. |
fee | Always 500. |
Tap an error to see its exact response.
{
"success": false,
"error": {
"code": "MISSING_PARAMS",
"message": "Required fields: meter_number, amount, phone, reference"
}
}{
"success": false,
"error": {
"code": "KYC_REQUIRED",
"message": "Approved KYC is required for bills and airtime."
}
}{
"success": false,
"error": {
"code": "LIMIT",
"message": "Maximum is UGX 5,000,000 per transaction."
}
}{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. You need UGX 38,500 but have UGX 3,000."
}
}{
"success": false,
"error": {
"code": "PURCHASE_FAILED",
"message": "Purchase failed. Your wallet has been refunded."
}
}Authentication, rate limit and account errors (401, 403, 429) can happen on any endpoint — see error codes.
We POST a JSON event to your callback_url (and to webhooks saved in your dashboard) as soon as a transaction changes status.
payment.success · payment.failed | Result of a /collect or /collect-stanbic request. |
payout.success · payout.failed | Result of a /payout request. |
bill.success · bill.failed | Result of airtime, data, electricity, water or TV purchases. |
*.pending | Sent if a transaction is still processing (payment.pending, payout.pending, bill.pending). |
{
"event": "payment.success",
"timestamp": "2026-10-02T08:16:12+00:00",
"data": {
"transaction_id": "LWAK3F9QZ7A1C",
"reference": "ORDER-123",
"merchant_reference": "ORDER-123",
"amount": 5000,
"status": "completed",
"type": "deposit",
"phone": "0770000000",
"provider": "mtn",
"created_at": "2026-10-02 08:15:00"
}
}{
"event": "payment.failed",
"timestamp": "2026-10-02T08:17:40+00:00",
"data": {
"transaction_id": "LWAK3F9QZ7A1C",
"reference": "ORDER-123",
"merchant_reference": "ORDER-123",
"amount": 5000,
"status": "failed",
"type": "deposit",
"phone": "0770000000",
"provider": "mtn",
"created_at": "2026-10-02 08:15:00"
}
}X-LworxPay-Event | The event name, e.g. payment.success. |
X-LworxPay-Signature | HMAC-SHA256 of the raw request body using your secret (sent when a secret is set). |
User-Agent | LworxPay-Webhook/1.0 |
Recompute the signature from the raw body and compare. Reply with any 2xx status. If your server does not answer with success we retry up to 3 times, so make your handler safe to run twice (use transaction_id to skip duplicates).
<?php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, 'YOUR_WEBHOOK_SECRET');
if (!hash_equals($expected, $_SERVER['HTTP_X_LWORXPAY_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
if ($event['event'] === 'payment.success') {
// mark order $event['data']['reference'] as paid
}
http_response_code(200);const crypto = require("crypto");
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto.createHmac("sha256", process.env.WEBHOOK_SECRET).update(req.body).digest("hex");
const sent = req.get("X-LworxPay-Signature") || "";
if (sent.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
if (event.event === "payment.success") {
// mark order event.data.reference as paid
}
res.sendStatus(200);
});| Action | Fee |
|---|---|
| Collect (/collect) | 4% added on top — paid by the customer, not taken from you. |
| Collect via Stanbic (/collect-stanbic) | Free. No fee added for the customer and nothing taken from you. |
| Payout (/payout) | UGX 500 for amounts up to 5,000 · 10% for 5,001 – 9,999 · 4% for 10,000 and above. Taken from your wallet on top of the amount. A custom fee may apply to your account. |
| Airtime & data bundles | Free. |
| Electricity, water, TV | UGX 500 per payment. |
| Limit per bill / airtime purchase | UGX 5,000,000. |
All errors use the same shape and the HTTP status shown below.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | AUTHENTICATION_FAILED | API key missing, wrong or inactive. |
| 403 | ACCOUNT_SUSPENDED | Your account is suspended. Contact support. |
| 403 | IP_NOT_ALLOWED | Your server IP is not on your IP whitelist. |
| 429 | RATE_LIMITED | More than 60 requests in one minute. |
| 400 | MISSING_PARAMS | A required field is missing. |
| 400 | DUPLICATE_REFERENCE | You already used this reference. |
| 400 | INVALID_PHONE | Phone number is not a valid Ugandan number. |
| 400 | INVALID_AMOUNT | Amount is below the minimum. |
| 400 | LIMIT | Amount is above the UGX 5,000,000 limit. |
| 400 | INSUFFICIENT_BALANCE | Wallet cannot cover the amount plus fee. |
| 400 | INVALID_BUNDLE | bundle_id is not in the catalogue. |
| 400 | VERIFICATION_FAILED | The network could not verify the number. |
| 400 | PAYMENT_FAILED / PAYOUT_FAILED / PURCHASE_FAILED | The mobile money network rejected the request. Wallet changes are reversed automatically. |
| 403 | KYC_REQUIRED | Complete KYC verification first. |
| 403 | ACCOUNT_COOLDOWN | New accounts wait 24 hours before payouts. |
| 403 | DEPOSIT_DISABLED / WITHDRAWAL_DISABLED | Switched off in your API settings or on your account. |
| 403 | DEPOSITS_BLOCKED / WITHDRAWALS_BLOCKED | Temporarily paused platform-wide. |
| 404 | NOT_FOUND | Unknown transaction or endpoint. |
| 503 | GATEWAY_UNAVAILABLE | /collect-stanbic only: the gateway is temporarily unavailable. Safe to retry. |
| 504 | GATEWAY_TIMEOUT | /collect-stanbic only: no answer from the gateway. The prompt may have been sent, so check /status with your reference before retrying. |
Need help integrating? Email supports@lworx-pay.com.