Developer Documentation
CtechPay API Integration
Build secure payment experiences with Airtel Money and Card APIs. This guide covers the full flow from initiation to status verification and transaction details retrieval.
Getting Started
Before integrating, make sure your merchant account is active and your service API token is generated from the dashboard.
- Registered CtechPay merchant account
- Service API token
- Server-side backend to secure credentials
Production
https://new-api.ctechpay.com
Sandbox
Coming soon
Authentication
All API calls require your service token. Include it in request payload or query params depending on endpoint specification.
Never expose API tokens in frontend code. Keep tokens in backend environment variables.
Official SDKs
Use the official CtechPay packages to create hosted payment links, initiate Airtel Money payments, and check transaction status without manually wiring every HTTP request.
For card payments, use the Hosted Payment Page so CtechPay securely handles card collection and authentication. For mobile apps, the Flutter SDK opens card checkout inside the app and polls status by order reference.
For production mobile apps, avoid embedding long-lived service tokens directly in public app builds. Use your backend to create payments or issue integration-specific credentials where applicable.
PHP Example
use CtechPay\CtechPay;
$ctechpay = CtechPay::client('YOUR_API_TOKEN_HERE');
$payment = $ctechpay->hostedPayments()->create([
'amount' => 5000,
'customer_reference' => 'LOANREFE123',
'customer_message' => 'Loan repayment for March',
'redirectUrl' => 'https://yoursite.com/payment-complete',
'cancelUrl' => 'https://yoursite.com/payment-cancelled',
]);
return redirect($payment['data']['hosted_payment_url']);Node.js Example
import CtechPay from '@ctechpay/ctechpay-js';
const ctechpay = CtechPay.client(process.env.CTECHPAY_TOKEN);
const payment = await ctechpay.hostedPayments.create({
amount: 5000,
customer_reference: 'LOANREFE123',
customer_message: 'Loan repayment for March',
redirectUrl: 'https://yoursite.com/payment-complete',
cancelUrl: 'https://yoursite.com/payment-cancelled',
});
console.log(payment.data.hosted_payment_url);Python Example
from ctechpay import CtechPay
ctechpay = CtechPay.client("YOUR_API_TOKEN_HERE")
payment = ctechpay.hosted_payments.create({
"amount": 5000,
"customer_reference": "LOANREFE123",
"customer_message": "Loan repayment for March",
"redirectUrl": "https://yoursite.com/payment-complete",
"cancelUrl": "https://yoursite.com/payment-cancelled",
})
print(payment["data"]["hosted_payment_url"])Flutter Example
import 'package:ctechpay/ctechpay.dart';
import 'package:flutter/material.dart';
final ctechpay = CtechPayClient(
token: 'YOUR_API_TOKEN_HERE',
);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CtechPayCheckoutPage(
client: ctechpay,
amount: 5000,
customerReference: 'LOANREFE123',
customerMessage: 'Loan repayment for March',
onCompleted: (result) {
print('Airtel payment completed: ${result.transactionId}');
},
onCardCompleted: (result) {
print('Card payment completed: ${result.orderReference}');
},
onFailed: (error) {
print('Payment failed: $error');
},
onCancelled: () {
print('Payment cancelled');
},
),
),
);Flutter Airtel Polling
final result = await ctechpay.airtel.payAndPoll(
amount: 5000,
phone: '0999123456',
customerReference: 'LOANREFE123',
customerMessage: 'Loan repayment for March',
onPoll: (status) {
print('Latest Airtel status: $status');
},
);
if (result.isCompleted) {
print('Paid: ${result.transactionId}');
}Flutter Card Status Check
final checkout = await ctechpay.cards.createPaymentPage( amount: 5000, customerReference: 'LOANREFE123', customerMessage: 'Loan repayment for March', ); print(checkout.paymentPageUrl); final finalResult = await ctechpay.cards.pollHostedStatus( orderReference: checkout.orderReference, ); print(finalResult.status);
WooCommerce Plugin
Use the CtechPay WooCommerce plugin when you want to accept Airtel Money and card payments from a WordPress online store without writing custom checkout code.
Download: https://github.com/Laughwellreformed/ctechpay-payments-for-woocommerce/releases/download/v0.1.2/ctechpay-payments-for-woocommerce-v0.1.2.zip Install: 1. Log in to WordPress Admin 2. Go to Plugins > Add New > Upload Plugin 3. Upload the downloaded ZIP file 4. Activate CtechPay for WooCommerce 5. Go to WooCommerce > Settings > Payments > CtechPay 6. Enable the payment method and enter your CtechPay service token
The plugin supports classic WooCommerce checkout and the modern WooCommerce Checkout Block. Customers are redirected to the secure CtechPay hosted checkout, then WooCommerce verifies payment before updating the order.
Hosted Payment Page
Create a hosted checkout session when you want CtechPay to show the customer a payment page.
Step 1 - Create Hosted Payment
/api/v1/hosted/payment
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Required | Service API token |
amount | numeric | Required | Amount in MWK |
category_flag | string | Optional | Merchant category flag |
customer_reference | string | Optional | Your own transaction reference |
customer_message | string | Optional | Message shown on the hosted page |
customer_name | string | Optional | Customer name for your records |
customer_email | Optional | Customer email for your records | |
redirectUrl | url | Optional | Where to send the customer after a successful hosted payment |
cancelUrl | url | Optional | Where to send the customer after cancelling hosted checkout |
curl -X POST "https://new-api.ctechpay.com/api/v1/hosted/payment" \
-H "Content-Type: application/json" \
-d '{
"token": "YOUR_API_TOKEN_HERE",
"amount": 5000,
"category_flag": "LOAN_PAYMENT",
"customer_reference": "LOANREFE123",
"customer_message": "Loan repayment for March",
"redirectUrl": "https://yoursite.com/payment-complete",
"cancelUrl": "https://yoursite.com/payment-cancelled"
}'// Response
{
"status": "success",
"message": "Hosted payment page created successfully.",
"data": {
"reference": "HPABC123...",
"amount": 5000,
"currency": "MWK",
"status": "pending",
"hosted_payment_token": "TOKEN123...",
"hosted_payment_url": "https://new-api.ctechpay.com/hosted/payment/TOKEN123...",
"redirect_url": "https://yoursite.com/payment-complete",
"cancel_url": "https://yoursite.com/payment-cancelled",
"expires_at": "2026-07-12T10:00:00.000000Z"
}
}Check Hosted Payment Status
/api/v1/hosted/status
Use the hosted payment reference returned during creation, for example HPPCRBPXRKS9G6F7, to check the payment status without knowing whether the customer selected Airtel Money or card.
curl -X POST "https://new-api.ctechpay.com/api/v1/hosted/status" \
-H "Content-Type: application/json" \
-d '{
"token": "YOUR_API_TOKEN_HERE",
"reference": "HPPCRBPXRKS9G6F7"
}'// Response
{
"status": "paid",
"selected_method": "airtel",
"reference": "HPPCRBPXRKS9G6F7",
"hosted_reference": "HPPCRBPXRKS9G6F7",
"transaction_reference": "ID26032322250002efCTPAY",
"amount": 5000,
"currency": "MWK",
"trans_id": "ID26032322250002efCTPAY",
"card_order_reference": null,
"a_trans_status": "TS"
}Successful Redirect Reference
When a hosted payment completes successfully and you supplied redirectUrl, CtechPay redirects the customer back with a reference query parameter appended.
https://yoursite.com/payment-complete?reference=TRANSACTION_OR_ORDER_REFERENCE
| Payment Method | reference value | Status check API |
|---|---|---|
| Airtel Money | Airtel Money trans_id |
POST /api/v1/airtel/transaction/details with transaction_id set to the redirect reference |
| Visa / Mastercard | Card order_reference |
GET /api/v1/orders/status with orderRef set to the redirect reference |
Mobile Money API
Recommended flow: Initiate payment - Check status - Fetch full details.
Step 1 - Initiate Payment
/api/v1/airtel/payment
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Required | Service API token |
amount | numeric | Required | Amount in MWK |
phone | string | Required | Airtel number (e.g. 0999123456) |
category_flag | string | Optional | Merchant category flag |
customer_reference | string | Optional | Your own defined reference for the transaction |
customer_message | string | Optional | Your own message/description for the transaction |
Step 1 Request Examples (Multi-language)
cURL
curl -X POST "https://new-api.ctechpay.com/api/v1/airtel/payment" \
-H "Content-Type: application/json" \
-d '{
"token": "YOUR_API_TOKEN_HERE",
"amount": 5000,
"phone": "0999123456",
"category_flag": "LOAN_PAYMENT",
"customer_reference": "LOANREFE123",
"customer_message": "Loan repayment for March",
}'Guzzle PHP
$client = new \GuzzleHttp\Client(); $response = $client->post('https://new-api.ctechpay.com/api/v1/airtel/payment', [ 'json' => [ 'token' => 'YOUR_API_TOKEN_HERE', 'amount' => 5000, 'phone' => '0999123456', 'category_flag' => 'LOAN_PAYMENT', 'customer_reference' => 'LOANREFE123', 'customer_message' => 'Loan repayment for March', ], ]); $body = $response->getBody()->getContents();
PHP cURL
$payload = [ 'token' => 'YOUR_API_TOKEN_HERE', 'amount' => 5000, 'phone' => '0999123456', 'category_flag' => 'LOAN_PAYMENT', 'customer_reference' => 'LOANREFE123', 'customer_message' => 'Loan repayment for March', ]; $ch = curl_init('https://new-api.ctechpay.com/api/v1/airtel/payment'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch); curl_close($ch);
Python (requests)
import requests
url = "https://new-api.ctechpay.com/api/v1/airtel/payment"
payload = {
"token": "YOUR_API_TOKEN_HERE",
"amount": 5000,
"phone": "0999123456",
"category_flag": "LOAN_PAYMENT",
"customer_reference": "LOANREFE123",
"customer_message": "Loan repayment for March",
}
r = requests.post(url, json=payload, timeout=30)
print(r.status_code)
print(r.json())Node.js (axios)
const axios = require('axios');
async function initiateMobilePayment() {
const url = 'https://new-api.ctechpay.com/api/v1/airtel/payment';
const payload = {
token: 'YOUR_API_TOKEN_HERE',
amount: 5000,
phone: '0999123456',
category_flag: 'LOAN_PAYMENT'
customer_reference: 'LOANREFE123',
customer_message: 'Loan repayment for March',
};
const { data } = await axios.post(url, payload, {
headers: { 'Content-Type': 'application/json' },
timeout: 30000
});
console.log(data);
}
initiateMobilePayment().catch(console.error);// Sample Response { "status": "success", "message": "Payment initiated successfully.", "data": { "data": { "transaction": { "id": "ID26032322250002efCTPAY", "status": "Success." } }, "status": { "response_code": "DP00800001006", "code": "200", "success": true, "result_code": "ESB000010", "message": "SUCCESS" } } "customer_reference": "LOANREFE123", "customer_message": "Loan repayment for March", }
Initiate Response Field Reference
| Field | Type | Description |
|---|---|---|
status | string | Top-level outcome: success or error. |
message | string | Human-readable result of the initiation request. |
data.data.transaction.id | string | CtechPay transaction ID. Save this value for Steps 2 and 3. |
data.data.transaction.status | string | Airtel network initiation status. Success. means push notification was dispatched. |
data.status.response_code | string | Airtel gateway response code. DP00800001006 means initiated successfully. |
data.status.code | string | HTTP-equivalent status code from Airtel gateway. |
data.status.success | boolean | true if initiation was accepted by Airtel network. |
data.status.result_code | string | ESB result code. ESB000010 indicates success. |
data.status.message | string | ESB outcome message (for example SUCCESS). |
customer_reference | string | Customer reference provided during payment initiation. |
customer_message | string | Customer message provided during payment initiation. |
After initiating payment, the customer receives a push notification on Airtel Money to approve. Payment typically completes in 30-60 seconds. Save data.data.transaction.id because it is required for the next two steps.
Mobile Status Check
Use the transaction ID returned from the Mobile Money initiation response to poll payment status and retrieve full Airtel transaction details.
Step 2 - Check Payment Status
/api/v1/airtel/status
| Parameter | Type | Required | Description |
|---|---|---|---|
trans_id | string | Required | Transaction ID from initiate response |
Step 2 Request Examples (Multi-language)
cURL
curl -X POST "https://new-api.ctechpay.com/api/v1/airtel/status" \
-H "Content-Type: application/json" \
-d '{
"trans_id": "ID26032322250002efCTPAY"
}'Guzzle PHP
$client = new \GuzzleHttp\Client(); $response = $client->post('https://new-api.ctechpay.com/api/v1/airtel/status', [ 'json' => [ 'trans_id' => 'ID26032322250002efCTPAY', ], ]); $body = $response->getBody()->getContents();
PHP cURL
$payload = [ 'trans_id' => 'ID26032322250002efCTPAY', ]; $ch = curl_init('https://new-api.ctechpay.com/api/v1/airtel/status'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch); curl_close($ch);
Python (requests)
import requests
url = "https://new-api.ctechpay.com/api/v1/airtel/status"
payload = {"trans_id": "ID26032322250002efCTPAY"}
r = requests.post(url, json=payload, timeout=30)
print(r.status_code)
print(r.json())Node.js (axios)
const axios = require('axios');
async function checkMobileStatus() {
const url = 'https://new-api.ctechpay.com/api/v1/airtel/status';
const payload = { trans_id: 'ID26032322250002efCTPAY' };
const { data } = await axios.post(url, payload, {
headers: { 'Content-Type': 'application/json' },
timeout: 30000
});
console.log(data);
}
checkMobileStatus().catch(console.error);// Sample Response { "response_code": "DP00800001001", "result_code": "ESB000010", "transaction_status": "TS", "airtel_money_id": "MP260119.0848.B17208", "message": "Your transaction has been successfully processed" }
Status Response Field Reference
| Field | Type | Description |
|---|---|---|
response_code | string | Airtel gateway response code. DP00800001001 means success. |
result_code | string | ESB result code. ESB000010 means transaction successful. |
transaction_status | string | Short status code: TS = successful, TF = failed. |
airtel_money_id | string | Airtel Money reference ID for the transaction. |
message | string | Human-readable outcome message. |
Poll the status endpoint every 5-10 seconds for up to 90 seconds. If transaction_status is still not TS after that window, treat it as pending and advise the customer to check their Airtel Money balance before retrying.
Mobile Error Scenarios
Below are common mobile integration errors and what they usually mean:
| Endpoint | Status Code | Typical Cause | What to Check |
|---|---|---|---|
/api/v1/airtel/payment | 422 | Invalid phone format | Ensure Malawian number format and valid length. |
/api/v1/airtel/payment | 401 | Invalid or missing token | Confirm your Service API token is correct and active. |
/api/v1/airtel/status | 400 | Missing trans_id | Always pass the exact transaction ID returned during initiation. |
/api/v1/airtel/status | 422 | Validation failure | Check payload field names and types. |
Example Mobile Validation Error
{
"status": "error",
"message": "Invalid phone number format",
"errors": {
"phone": [
"The phone field must be a valid Malawian phone number"
]
}
}Step 3 - Get Full Transaction Details
/api/v1/airtel/transaction/details
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction_id | string | Required | Same trans_id used for status checks |
Step 3 Request Examples
Example Request (PHP - Guzzle)
$response = $client->post('https://new-api.ctechpay.com/api/v1/airtel/transaction/details', [
'json' => [
'transaction_id' => 'ID2601190847549183CTPAY'
]
]);
$details = json_decode($response->getBody(), true);cURL
curl -X POST "https://new-api.ctechpay.com/api/v1/airtel/transaction/details" \
-H "Content-Type: application/json" \
-d '{
"transaction_id": "ID2601190847549183CTPAY"
}'PHP cURL
$payload = [
'transaction_id' => 'ID2601190847549183CTPAY'
];
$ch = curl_init('https://new-api.ctechpay.com/api/v1/airtel/transaction/details');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$details = json_decode($response, true);Python (requests)
import requests
url = "https://new-api.ctechpay.com/api/v1/airtel/transaction/details"
payload = {"transaction_id": "ID2601190847549183CTPAY"}
r = requests.post(url, json=payload, timeout=30)
print(r.status_code)
print(r.json())Node.js (axios)
const axios = require('axios');
async function fetchTransactionDetails() {
const url = 'https://new-api.ctechpay.com/api/v1/airtel/transaction/details';
const payload = { transaction_id: 'ID2601190847549183CTPAY' };
const { data } = await axios.post(url, payload, {
headers: { 'Content-Type': 'application/json' },
timeout: 30000
});
console.log(data);
}
fetchTransactionDetails().catch(console.error);// Sample Response { "status": "success", "trans_id": "ID2601190847549183CTPAY", "airtel_money_id": "MP260119.0848.B17208", "transaction_status": "completed", "phone_number": "998757521", "result_code": "ESB000010", "response_code": "DP00800001001", "a_trans_status": "TS", "message": "Your transaction has been successfully processed" }
Transaction Details Field Reference
| Field | Type | Description |
|---|---|---|
status | string | API call outcome: success or error. |
trans_id | string | CtechPay transaction ID (echoed back). |
airtel_money_id | string | Airtel Money network reference. Keep this for reconciliation. |
transaction_status | string | Verbose status: completed or failed. |
phone_number | string | Customer's phone number as registered on Airtel Money. |
result_code | string | ESB result code (ESB000010 = success). |
response_code | string | Airtel gateway response code (DP00800001001 = success). |
a_trans_status | string | Short Airtel status code: TS = successful, TF = failed. |
message | string | Human-readable outcome message from the Airtel network. |
Card Payment API
Recommended flow: Create order - Redirect customer - Verify status/details.
Step 1 - Create Payment Order
/api/v1/orders
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Required | Your Service API token |
amount | numeric | Required | Payment amount in MWK |
category_flag | string | Optional | Payment category identifier |
customer_reference | string | Optional | Your own defined reference for the transaction |
customer_message | string | Optional | Your own message/description for the transaction |
merchantAttributes | boolean | Optional | Set to true to enable custom merchant attributes. |
redirectUrl | url | Optional | URL to redirect after successful payment (requires merchantAttributes=true). |
cancelUrl | url | Optional | URL to redirect if payment is cancelled (requires merchantAttributes=true). |
cancelText | string | Optional | Custom text for cancel button (requires merchantAttributes=true). |
skipConfirmationPage | boolean | Optional | Skip payment confirmation page (requires merchantAttributes=true). |
Step 1 Request Examples (Multi-language)
cURL
curl -X POST "https://new-api.ctechpay.com/api/v1/orders" \
-H "Content-Type: application/json" \
-d '{
"token": "YOUR_API_TOKEN_HERE",
"amount": 10000,
"category_flag": "SUBSCRIPTION",
"customer_reference": "ORDER123",
"customer_message": "Subscription payment for user 123",
"merchantAttributes": true,
"redirectUrl": "https://yoursite.com/payment/success",
"cancelUrl": "https://yoursite.com/payment/cancel",
"cancelText": "Go Back",
"skipConfirmationPage": false
}'Guzzle PHP
$client = new \GuzzleHttp\Client();
$response = $client->post('https://new-api.ctechpay.com/api/v1/orders', [
'json' => [
'token' => env('CTECHPAY_API_TOKEN'),
'amount' => 10000,
'category_flag' => 'SUBSCRIPTION',
'customer_reference' => 'ORDER123',
'customer_message' => 'Subscription payment for user 123',
'merchantAttributes' => true,
'redirectUrl' => 'https://yoursite.com/payment/success',
'cancelUrl' => 'https://yoursite.com/payment/cancel',
'cancelText' => 'Go Back',
'skipConfirmationPage' => false,
]
]);
$result = json_decode($response->getBody(), true);
header('Location: ' . $result['payment_page_URL']);
exit();PHP cURL
$payload = [
'token' => 'YOUR_API_TOKEN_HERE',
'amount' => 10000,
'category_flag' => 'SUBSCRIPTION',
'customer_reference' => 'ORDER123',
'customer_message' => 'Subscription payment for user 123',
'merchantAttributes' => true,
'redirectUrl' => 'https://yoursite.com/payment/success',
'cancelUrl' => 'https://yoursite.com/payment/cancel',
'cancelText' => 'Go Back',
'skipConfirmationPage' => false,
];
$ch = curl_init('https://new-api.ctechpay.com/api/v1/orders');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
header('Location: ' . $result['payment_page_URL']);
exit();Python (requests)
import requests
url = "https://new-api.ctechpay.com/api/v1/orders"
payload = {
"token": "YOUR_API_TOKEN_HERE",
"amount": 10000,
"category_flag": "SUBSCRIPTION",
"customer_reference": "ORDER123",
"customer_message": "Subscription payment for user 123",
"merchantAttributes": True,
"redirectUrl": "https://yoursite.com/payment/success",
"cancelUrl": "https://yoursite.com/payment/cancel",
"cancelText": "Go Back",
"skipConfirmationPage": False,
}
r = requests.post(url, json=payload, timeout=30)
result = r.json()
print(result["payment_page_URL"])Node.js (axios)
const axios = require('axios');
async function createCardOrder() {
const url = 'https://new-api.ctechpay.com/api/v1/orders';
const payload = {
token: 'YOUR_API_TOKEN_HERE',
amount: 10000,
category_flag: 'SUBSCRIPTION',
customer_reference: 'ORDER123',
customer_message: 'Subscription payment for user 123',
merchantAttributes: true,
redirectUrl: 'https://yoursite.com/payment/success',
cancelUrl: 'https://yoursite.com/payment/cancel',
cancelText: 'Go Back',
skipConfirmationPage: false
};
const { data } = await axios.post(url, payload, {
headers: { 'Content-Type': 'application/json' },
timeout: 30000
});
console.log(data.payment_page_URL);
}
createCardOrder().catch(console.error);// Success Response { "order_reference": "ORD123456789", "payment_page_URL": "https://payment.gateway.com/pay/xyz123...", "customer_reference": "ORDER123", "customer_message": "Subscription payment for user 123" }
1. Create an order via API and save the order_reference
2. Redirect the customer to payment_page_URL
3. Customer completes payment on the secure hosted page
4. Customer is returned to your redirectUrl or cancelUrl
5. Verify payment using the status or details endpoints below
Set merchantAttributes to true to enable custom redirect URLs, cancel URLs, and confirmation behavior. skip3DS remains false for security.
Card Status Check
Use the card order_reference returned from order creation to verify whether the customer completed payment on the hosted card checkout.
Step 2 - Check Card Order Status
Quickly confirm whether the card payment was completed. Pass the order_reference returned in Step 1.
/api/v1/orders/status
| Parameter | Type | Required | Description |
|---|---|---|---|
orderRef | string | Yes | The order reference returned when creating the order |
token | string | Yes | Your Service API token |
Status Request Examples (Multi-language)
Example Request (PHP - Guzzle)
$response = $client->get('https://new-api.ctechpay.com/api/v1/orders/status', [
'query' => [
'orderRef' => 'ORD123456789',
'token' => env('CTECHPAY_API_TOKEN')
]
]);
$status = json_decode($response->getBody(), true);cURL
curl -G "https://new-api.ctechpay.com/api/v1/orders/status" \ --data-urlencode "orderRef=ORD123456789" \ --data-urlencode "token=YOUR_API_TOKEN_HERE"
PHP cURL
$url = 'https://new-api.ctechpay.com/api/v1/orders/status?' . http_build_query([
'orderRef' => 'ORD123456789',
'token' => 'YOUR_API_TOKEN_HERE',
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$status = json_decode($response, true);Python (requests)
import requests
url = "https://new-api.ctechpay.com/api/v1/orders/status"
params = {
"orderRef": "ORD123456789",
"token": "YOUR_API_TOKEN_HERE",
}
r = requests.get(url, params=params, timeout=30)
print(r.status_code)
print(r.json())Node.js (axios)
const axios = require('axios');
async function checkCardStatus() {
const url = 'https://new-api.ctechpay.com/api/v1/orders/status';
const { data } = await axios.get(url, {
params: {
orderRef: 'ORD123456789',
token: 'YOUR_API_TOKEN_HERE'
},
timeout: 30000
});
console.log(data);
}
checkCardStatus().catch(console.error);// Success Response { "_id": "ORD123456789", "orderReference": "REF456789", "status": "COMPLETED", "currencyCode": "MWK", "amount": 10000, "formattedAmount": "MWK 10000", "cardHolderName": "John Doe" }
Step 3 - Get Full Order Details
Retrieve the complete record for a specific order, including the category flag, batch ID, and formatted amount. Replace {order_id} with the UUID returned as order_reference in Step 1.
/api/v1/orders/details/{order_id}
Order Details Request Examples (Multi-language)
Example Request (PHP)
$orderId = "32aa1523-c105-472b-b0c5-fac0fcf6b6e2";
$response = $client->get("https://new-api.ctechpay.com/api/v1/orders/details/{$orderId}");
$order = json_decode($response->getBody(), true);Example Request (JavaScript)
const orderId = '32aa1523-c105-472b-b0c5-fac0fcf6b6e2';
const response = await fetch(`https://new-api.ctechpay.com/api/v1/orders/details/${orderId}`);
const order = await response.json();
console.log(order);cURL
curl "https://new-api.ctechpay.com/api/v1/orders/details/32aa1523-c105-472b-b0c5-fac0fcf6b6e2"
PHP cURL
$orderId = '32aa1523-c105-472b-b0c5-fac0fcf6b6e2';
$ch = curl_init("https://new-api.ctechpay.com/api/v1/orders/details/{$orderId}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
$order = json_decode($response, true);Python (requests)
import requests
order_id = "32aa1523-c105-472b-b0c5-fac0fcf6b6e2"
url = f"https://new-api.ctechpay.com/api/v1/orders/details/{order_id}"
r = requests.get(url, timeout=30)
print(r.status_code)
print(r.json())Node.js (axios)
const axios = require('axios');
async function getOrderDetails() {
const orderId = '32aa1523-c105-472b-b0c5-fac0fcf6b6e2';
const url = `https://new-api.ctechpay.com/api/v1/orders/details/${orderId}`;
const { data } = await axios.get(url, { timeout: 30000 });
console.log(data);
}
getOrderDetails().catch(console.error);// Success Response { "order_id": "32aa1523-c105-472b-b0c5-fac0fcf6b6e2", "reference": null, "currencyCode": "MWK", "amount": 150000, "category_flag": null, "batch_id": null, "formattedAmount": "MWK 150000", "status": "PURCHASED", "card_holder": "Tausifbeg Mirza" }
Order Details Field Reference
| Field | Type | Description |
|---|---|---|
order_id | string (UUID) | Unique order identifier on the CtechPay platform. |
reference | string / null | Merchant-supplied reference, if provided at creation. |
currencyCode | string | ISO 4217 currency code (always MWK for Malawi). |
amount | numeric | Payment amount in MWK. |
category_flag | string / null | Category flag assigned to the order, if any. |
batch_id | integer / null | Settlement batch ID, populated after settlement. |
formattedAmount | string | Human-readable amount string (for example MWK 150000). |
status | string | Order status: PURCHASED, STARTED, FAILED, etc. |
card_holder | string | Name of the cardholder as entered during payment. |
Payment Categories
Organize and track your transactions by assigning them to different categories.
Payment categories allow you to organize transactions for better reporting and analysis. For example, a microfinance institution might use categories like LOAN_DISBURSEMENT, LOAN_RECOVERY, and LOAN_APPLICATION_FEES.
Using Categories
To use categories, first create them in your dashboard:
- Log in to your CtechPay Dashboard
- Navigate to Settings -> Payment Categories
- Create a new category with a name and flag
- Use the category flag in your API requests
Use category_flag to segment transaction reports by business use case.
Financial
LOAN_APPLICATION_FEESLOAN_RECOVERYLOAN_DISBURSEMENT
E-commerce
PRODUCT_PURCHASESUBSCRIPTION_FEESHIPPING_CHARGES
Services
CONSULTATION_FEESERVICE_CHARGEMEMBERSHIP_FEE
Error Handling
The API returns standard HTTP status codes and error messages to help you troubleshoot issues.
Common Status Codes
| Code | Status | Description |
|---|---|---|
200 | OK | Request successful |
400 | Bad Request | Invalid parameters or missing required fields |
401 | Unauthorized | Invalid or missing API token |
422 | Unprocessable Entity | Validation error (check error details) |
500 | Internal Server Error | Something went wrong on our end |
Error Response Format
{
"status": "error",
"message": "Invalid phone number format",
"errors": {
"phone": [
"The phone field must be a valid Malawian phone number"
]
}
}Postman Collection
Use this quick starter collection for testing core endpoints.
{
"info": {
"name": "CtechPay API",
"description": "Complete CtechPay API collection for mobile money and card payments",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{"key": "base_url", "value": "https://new-api.ctechpay.com", "type": "string"},
{"key": "api_token", "value": "YOUR_API_TOKEN_HERE", "type": "string"}
],
"item": [
{
"name": "Mobile Money Payments",
"item": [
{
"name": "Initiate Mobile Payment",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"token\": \"{{api_token}}\",\n \"amount\": 5000,\n \"phone\": \"0999123456\",\n \"category_flag\": \"LOAN_PAYMENT\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/airtel/payment",
"host": ["{{base_url}}"],
"path": ["api", "v1", "airtel", "payment"]
},
"description": "Initiate a mobile money payment from an Airtel Money customer."
}
},
{
"name": "Check Payment Status",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"trans_id\": \"ID2601190847549183CTPAY\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/airtel/status",
"host": ["{{base_url}}"],
"path": ["api", "v1", "airtel", "status"]
},
"description": "Check the status of a mobile money payment."
}
},
{
"name": "Get Transaction Details",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"transaction_id\": \"ID2601190847549183CTPAY\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/airtel/transaction/details",
"host": ["{{base_url}}"],
"path": ["api", "v1", "airtel", "transaction", "details"]
},
"description": "Get full mobile transaction details by transaction_id."
}
}
]
},
{
"name": "Card Payments",
"item": [
{
"name": "Create Payment Order",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"token\": \"{{api_token}}\",\n \"amount\": 10000,\n \"category_flag\": \"SUBSCRIPTION\",\n \"merchantAttributes\": true,\n \"redirectUrl\": \"https://yoursite.com/payment/success\",\n \"cancelUrl\": \"https://yoursite.com/payment/cancel\",\n \"cancelText\": \"Go Back\",\n \"skipConfirmationPage\": false\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/orders",
"host": ["{{base_url}}"],
"path": ["api", "v1", "orders"]
},
"description": "Create a card payment order and receive payment_page_URL for redirect."
}
},
{
"name": "Check Card Order Status",
"request": {
"method": "GET",
"url": {
"raw": "{{base_url}}/api/v1/ordersstatus?orderRef=ORD123456789&token={{api_token}}",
"host": ["{{base_url}}"],
"path": ["api", "v1", "orders", "status"],
"query": [
{"key": "orderRef", "value": "ORD123456789"},
{"key": "token", "value": "{{api_token}}"}
]
},
"description": "Quickly confirm whether card payment is completed."
}
},
{
"name": "Get Order Details",
"request": {
"method": "GET",
"url": {
"raw": "{{base_url}}/api/v1/ordersdetails/32aa1523-c105-472b-b0c5-fac0fcf6b6e2",
"host": ["{{base_url}}"],
"path": ["api", "v1", "orders", "details", "32aa1523-c105-472b-b0c5-fac0fcf6b6e2"]
},
"description": "Retrieve full order details including category_flag, batch_id, and formattedAmount."
}
}
]
},
{
"name": "Examples with Categories",
"item": [
{
"name": "Loan Application Fee",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"token\": \"{{api_token}}\",\n \"amount\": 2500,\n \"phone\": \"0999123456\",\n \"category_flag\": \"LOAN_APPLICATION_FEES\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/airtel/payment",
"host": ["{{base_url}}"],
"path": ["api", "v1", "airtel", "payment"]
}
}
},
{
"name": "Loan Recovery",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"token\": \"{{api_token}}\",\n \"amount\": 15000,\n \"phone\": \"0888234567\",\n \"category_flag\": \"LOAN_RECOVERY\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/airtel/payment",
"host": ["{{base_url}}"],
"path": ["api", "v1", "airtel", "payment"]
}
}
},
{
"name": "Product Purchase (Card)",
"request": {
"method": "POST",
"header": [
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"mode": "raw",
"raw": "{\n \"token\": \"{{api_token}}\",\n \"amount\": 8500,\n \"category_flag\": \"PRODUCT_PURCHASE\",\n \"merchantAttributes\": true,\n \"redirectUrl\": \"https://yourstore.com/order/success\",\n \"cancelUrl\": \"https://yourstore.com/cart\",\n \"cancelText\": \"Go Back\",\n \"skipConfirmationPage\": false\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/orders",
"host": ["{{base_url}}"],
"path": ["api", "v1", "orders"]
}
}
}
]
}
]
}
Merchant Authentication API
Login to obtain a Bearer token for accessing protected merchant endpoints like Account Information, Transactions, Settlements, and more.
Login - Get Bearer Token
/api/merchant/v1/auth/login
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Required | Merchant email address |
password | string | Required | Merchant password |
Request Examples
cURL
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/auth/login" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "merchant@example.com",
"password": "your_password"
}'JavaScript (Fetch)
fetch('https://new-api.ctechpay.com/api/merchant/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: 'merchant@example.com',
password: 'your_password'
})
})
.then(response => response.json())
.then(data => {
const token = data.data.token;
// Store token securely for subsequent requests
console.log('Bearer Token:', token);
})
.catch(error => console.error('Error:', error));PHP (Laravel HTTP Client)
use Illuminate\Support\Facades\Http;
$response = Http::post('https://new-api.ctechpay.com/api/merchant/v1/auth/login', [
'email' => 'merchant@example.com',
'password' => 'your_password'
]);
$token = $response->json()['data']['token'];
// Store token in session or secure storagePython (Requests)
import requests
data = {
'email': 'merchant@example.com',
'password': 'your_password'
}
response = requests.post(
'https://new-api.ctechpay.com/api/merchant/v1/auth/login',
json=data,
headers={'Accept': 'application/json'}
)
token = response.json()['data']['token']
# Store token for subsequent requestsSuccess Response
{
"success": true,
"message": "Login successful.",
"data": {
"token": "1|abc123def456xyz789...",
"user": {
"username": "merchant_user",
"firstname": "John",
"lastname": "Doe",
"email": "merchant@example.com",
"company_name": "Doe Enterprises Ltd",
"phone_number": "0888123456",
"avatar_url": "https://new-api.ctechpay.com/storage/avatars/avatar.png",
"initials": "JD",
"masked_id": "CTECH-MERCHANT-A1B2C3D4"
}
}
}Error Responses
401 Unauthorized - Invalid Credentials
{
"success": false,
"message": "Invalid credentials."
}403 Forbidden - Not a Merchant
{
"success": false,
"message": "Access denied. Only merchants can use this app."
}• Store tokens securely (never in frontend code or localStorage for web)
• Use HTTPS only for all API requests
• Tokens remain valid until logout or expiration
• Include token in Authorization header as: "Bearer YOUR_TOKEN"
Other Authentication Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/merchant/v1/auth/logout | POST | Logout and invalidate current token |
/api/merchant/v1/auth/profile | GET | Get authenticated merchant profile |
/api/merchant/v1/auth/change-password | POST | Change merchant password |
Change Password — Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
current_password | string | Required | The merchant's current password |
new_password | string | Required | New password (minimum 8 characters) |
new_password_confirmation | string | Required | Must match new_password |
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/auth/change-password" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"current_password": "oldpassword123",
"new_password": "newpassword456",
"new_password_confirmation": "newpassword456"
}'After successful login, use the token to access protected endpoints:
• Account Information
• Transactions & Receipts
• Bank Accounts
• Settlements
• Reports & Analytics
• Notifications
• Invoices
Account Information API
Retrieve comprehensive merchant account information including organization details, service charges, exemptions, and application credentials.
Get Account Information
/api/v1/account
Authentication: Bearer Token (from Merchant Login)
Request Example
cURL
curl -X GET "https://new-api.ctechpay.com/api/v1/account" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
JavaScript (Fetch)
fetch('https://new-api.ctechpay.com/api/v1/account', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));PHP (Laravel HTTP Client)
use Illuminate\Support\Facades\Http;
$response = Http::withToken('YOUR_TOKEN')
->get('https://new-api.ctechpay.com/api/v1/account');
$accountInfo = $response->json();Python (Requests)
import requests
headers = {
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json'
}
response = requests.get(
'https://new-api.ctechpay.com/api/v1/account',
headers=headers
)
account_info = response.json()Success Response
{
"status": "success",
"message": "Account information retrieved successfully",
"data": {
"account_info": {
"first_name": "John",
"last_name": "Doe",
"phone_number": "0888123456",
"email": "john@example.com",
"username": "johndoe",
"status": "ACTIVE"
},
"organization": {
"company_name": "Doe Enterprises Ltd",
"company_email": "info@doeenterprises.com",
"company_address": "123 Business St, Lilongwe",
"primary_phone": "0888123456",
"company_contacts": "0999654321"
},
"service_charge": {
"charge_percentage": 3.5,
"is_exempted": true,
"exemption_details": {
"fixed_amount_per_transaction": 500.00,
"exemption_note": "Flat fixed amount applies per transaction across all categories"
},
"exempted_categories": [
{
"name": "Loan Application Fees",
"flag": "LOAN_APPLICATION_FEES",
"fixed_amount": 200.00,
"is_default": true
},
{
"name": "Appeal Fees",
"flag": "APPEAL_FEES",
"fixed_amount": 150.00,
"is_default": false
}
]
},
"application": {
"app_name": "Loan Management System",
"app_type": "web",
"app_url": "https://loans.example.com",
"app_description": "System for managing loan applications",
"app_logo": "https://yoursite.com/storage/logos/app_logo.png",
"callback_url": "https://loans.example.com/callback",
"app_status": "approved",
"application_key": "app_abc123def456...",
"approved_at": "2026-03-15 10:30:00"
}
}
}Response for Non-Exempted Merchant
{
"status": "success",
"message": "Account information retrieved successfully",
"data": {
"account_info": {...},
"organization": {...},
"service_charge": {
"charge_percentage": 5.0,
"is_exempted": false,
"percentage_categories": [
{
"name": "General Payments",
"flag": "GENERAL_PAYMENTS",
"charge_percentage": 5.0,
"is_default": true
}
]
},
"application": null
}
}Error Responses
401 Unauthorized - Invalid or Missing Token
{
"message": "Unauthenticated."
}This endpoint requires a Bearer token. See Merchant Authentication section to obtain your token via the login endpoint.
Response Fields Explained
| Field | Type | Description |
|---|---|---|
account_info | object | Personal account details (name, email, phone, username, status) |
organization | object | Business/company information |
service_charge | object | Service charge configuration and exemption details |
service_charge.is_exempted | boolean | Whether merchant is exempt from percentage charges |
exempted_categories | array|null | Categories with fixed charge amounts (only if exempted) |
percentage_categories | array|null | Categories with percentage charges (only if not exempted) |
application | object|null | Approved application details with API credentials |
application.application_key | string | Application key used for programmatic access and webhook signature verification |
• Display merchant profile in mobile/web apps
• Show service charge information to users
• Access API credentials programmatically
• Verify organization details
• Check exemption status and category configurations
Webhooks
Webhooks notify your system when payment events happen in CtechPay. Configure your webhook URL from your approved application in the merchant dashboard. CtechPay sends an HTTP POST request with a JSON payload and signing headers.
Go to Applications, create or view your approved application, and set the Webhook URL. The URL must use HTTPS. The application key is also used as your webhook signing secret.
Delivery Behavior
| Item | Description |
|---|---|
payment_initiated | Sent immediately after CtechPay has created and submitted the payment request. |
payment_status_updated | Sent when CtechPay receives or confirms a final/updated payment status. |
| Success response | Your endpoint must return any HTTP 2xx response. |
| Retries | Failed webhook deliveries are retried up to 5 times with backoff. |
| Idempotency | Store and deduplicate by webhook_event_id. You can also deduplicate payment records by transaction_id or order_reference. |
Headers
| Header | Description |
|---|---|
CtechPay-Event | Webhook event name, for example payment_initiated. |
CtechPay-Delivery | Unique delivery/event ID. This matches webhook_event_id in the JSON payload. |
CtechPay-Timestamp | Unix timestamp used when signing the payload. |
CtechPay-Signature | Signature in the format t={timestamp},v1={hash}. |
Signature Verification
To verify a webhook, compute an HMAC SHA-256 hash using your application key as the secret. The signed content is:
{timestamp}.{raw_request_body}Compare your computed hash with the v1 value from CtechPay-Signature using a timing-safe comparison such as PHP's hash_equals.
Airtel Money Initiated Payload
{
"event": "payment_initiated",
"payment_method": "airtel_money",
"transaction_id": "ID260921091234abcdCTPAY",
"reference": "ID260921091234abcdCTPAY",
"amount": 1000,
"phone_number": "0999123456",
"category_flag": "LOAN_APPLICATION_FEES",
"old_status": null,
"new_status": "started",
"status": "started",
"response_code": "DP00800001006",
"result_code": "ESB000010",
"customer_reference": "LOAN-10001",
"customer_message": "Loan application fee",
"timestamp": "2026-09-21T09:09:43.846573Z",
"user_id": 10,
"application_key": "app_abc123def456...",
"webhook_event_id": "f1304617-6f77-4379-a12a-5f553885296d",
"webhook_event_type": "payment_initiated"
}Airtel Money Status Updated Payload
{
"event": "payment_status_updated",
"payment_method": "airtel_money",
"transaction_id": "ID260921091234abcdCTPAY",
"reference": "ID260921091234abcdCTPAY",
"amount": 1000,
"phone_number": "0999123456",
"old_status": "started",
"new_status": "completed",
"status": "completed",
"airtel_money_id": "AM123456789",
"response_code": "DP00800001000",
"result_code": "ESB000010",
"message": "Transaction successful",
"a_trans_status": "TS",
"customer_reference": "LOAN-10001",
"customer_message": "Loan application fee",
"timestamp": "2026-09-21T09:11:15.123456Z",
"user_id": 10,
"application_key": "app_abc123def456...",
"webhook_event_id": "ae8c78f1-2b13-4034-b9bf-735f5ed0196d",
"webhook_event_type": "payment_status_updated"
}Card Payment Payload
{
"event": "payment_status_updated",
"payment_method": "card",
"order_reference": "ORDER-260921-001",
"payment_reference": "PAY-REF-123",
"reference": "ORDER-260921-001",
"amount": 5000,
"category_flag": "GENERAL_PAYMENTS",
"old_status": "STARTED",
"new_status": "PURCHASED",
"status": "PURCHASED",
"gateway_state": "PURCHASED",
"card_holder": "JOHN DOE",
"customer_reference": "INV-10001",
"customer_message": "Invoice payment",
"timestamp": "2026-09-21T09:11:15.123456Z",
"user_id": 10,
"application_key": "app_abc123def456...",
"webhook_event_id": "4bff9c18-973a-4a99-a042-b2cf783f6b7d",
"webhook_event_type": "payment_status_updated"
}Test Webhook Payload
The dashboard's Test Webhook button sends a lightweight payload. It does not include user_id.
{
"event": "webhook.test",
"message": "This is a test webhook from CtechPay.",
"timestamp": "2026-09-21T09:09:43.846573Z",
"application_key": "app_abc123def456...",
"webhook_event_id": "f1304617-6f77-4379-a12a-5f553885296d",
"webhook_event_type": "webhook.test"
}Full PHP Webhook Receiver
Create an index.php on your HTTPS server and set its URL as your application's Webhook URL.
<?php
// index.php
header('Content-Type: application/json');
$secret = 'PASTE_YOUR_APPLICATION_KEY_HERE';
$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);
if (!is_array($payload)) {
http_response_code(400);
file_put_contents(
__DIR__ . '/webhook.log',
date('c') . " INVALID JSON\n" . $rawBody . "\n\n",
FILE_APPEND
);
echo json_encode([
'status' => 'error',
'message' => 'Invalid JSON',
]);
exit;
}
$signatureHeader = $_SERVER['HTTP_CTECHPAY_SIGNATURE'] ?? '';
$timestampHeader = $_SERVER['HTTP_CTECHPAY_TIMESTAMP'] ?? '';
$eventHeader = $_SERVER['HTTP_CTECHPAY_EVENT'] ?? '';
$deliveryHeader = $_SERVER['HTTP_CTECHPAY_DELIVERY'] ?? '';
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $signatureHeader, $matches);
$timestamp = $matches[1] ?? $timestampHeader;
$receivedSignature = $matches[2] ?? null;
$expectedSignature = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!$receivedSignature || !hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
file_put_contents(
__DIR__ . '/webhook.log',
date('c') . " INVALID SIGNATURE\n" . $rawBody . "\n\n",
FILE_APPEND
);
echo json_encode([
'status' => 'error',
'message' => 'Invalid signature',
]);
exit;
}
file_put_contents(
__DIR__ . '/webhook.log',
date('c') . " WEBHOOK RECEIVED\n" .
"Event: " . $eventHeader . "\n" .
"Delivery: " . $deliveryHeader . "\n" .
"Payload: " . json_encode($payload, JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
switch ($payload['event'] ?? '') {
case 'payment_initiated':
// Mark the payment as started in your system.
break;
case 'payment_status_updated':
if (($payload['new_status'] ?? $payload['status'] ?? null) === 'completed') {
// Mark Airtel Money payment as paid.
}
if (($payload['new_status'] ?? $payload['status'] ?? null) === 'PURCHASED') {
// Mark card payment as paid.
}
if (in_array(($payload['new_status'] ?? $payload['status'] ?? null), ['failed', 'FAILED'], true)) {
// Mark payment as failed.
}
break;
case 'webhook.test':
// Test webhook received successfully.
break;
}
http_response_code(200);
echo json_encode([
'status' => 'received',
'event' => $payload['event'] ?? null,
'delivery' => $deliveryHeader,
]);Return HTTP 200 as soon as you have verified and stored the webhook. Do heavy work asynchronously in your own system. Slow endpoints can delay immediate payment_initiated webhook delivery.
Merchant Transactions API
Retrieve all transactions associated with your merchant account — both bank (card) and mobile money. All endpoints require a valid Bearer token.
All endpoints in this section require the Authorization: Bearer {token} header obtained from the Merchant Login endpoint.
List All Transactions
/api/merchant/v1/transactions
Returns a paginated list of all transactions (both bank and mobile) for the authenticated merchant. Supports optional query filters.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | Optional | Page number for pagination |
per_page | integer | Optional | Results per page (max 100) |
status | string | Optional | Filter by status (e.g. PURCHASED, completed, failed) |
category | string | Optional | Filter by category flag (e.g. LOAN_PAYMENT). Use uncategorized for transactions with no category |
from | date | Optional | Filter from date (YYYY-MM-DD) |
to | date | Optional | Filter to date (YYYY-MM-DD) |
search | string | Optional | Search by order ID, card holder, reference (bank) or trans ID, phone, Airtel Money ID (mobile) |
cURL
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions?page=1&per_page=20&from=2026-06-01&to=2026-06-30" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
PHP (Guzzle)
$response = $client->get('https://new-api.ctechpay.com/api/merchant/v1/transactions', [ 'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json'], 'query' => ['page' => 1, 'per_page' => 20, 'from' => '2026-06-01', 'to' => '2026-06-30'], ]); $data = json_decode($response->getBody(), true);
Python (requests)
import requests
headers = {'Authorization': 'Bearer YOUR_TOKEN', 'Accept': 'application/json'}
r = requests.get('https://new-api.ctechpay.com/api/merchant/v1/transactions',
headers=headers, params={'page': 1, 'per_page': 20})
print(r.json())Node.js (axios)
const { data } = await axios.get(
'https://new-api.ctechpay.com/api/merchant/v1/transactions',
{ headers: { Authorization: 'Bearer YOUR_TOKEN' }, params: { page: 1, per_page: 20 } }
);
console.log(data);Transaction Summary
/api/merchant/v1/transactions/summary
Returns aggregate totals such as total collected, total transactions, and breakdowns by status and type. Ideal for dashboard widgets.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/summary" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"total_transactions": 254,
"total_amount": 3850000,
"successful": 238,
"failed": 10,
"pending": 6,
"bank_total": 1950000,
"mobile_total": 1900000
}
}List Bank (Card) Transactions
/api/merchant/v1/transactions/bank
Returns only card/bank transactions for the merchant. Supports the same status, category, from, to, search, and per_page query parameters as the all-transactions endpoint.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/bank?page=1&status=PURCHASED&from=2026-06-01" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
List Mobile Money Transactions
/api/merchant/v1/transactions/mobile
Returns only Airtel Money transactions for the merchant. Supports the same status, category, from, to, search, and per_page query parameters.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/mobile?page=1&category=LOAN_PAYMENT" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Get Bank Transaction Detail
/api/merchant/v1/transactions/bank/{id}
Returns full details for a single card transaction. Replace {id} with the transaction ID.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/bank/32aa1523-c105-472b-b0c5-fac0fcf6b6e2" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"id": "32aa1523-c105-472b-b0c5-fac0fcf6b6e2",
"order_reference": "ORD123456789",
"amount": 10000,
"currency": "MWK",
"status": "PURCHASED",
"card_holder": "John Doe",
"category_flag": "SUBSCRIPTION",
"created_at": "2026-06-01 10:30:00"
}
}Get Mobile Transaction Detail
/api/merchant/v1/transactions/mobile/{id}
Returns full details for a single Airtel Money transaction. Replace {id} with the transaction ID.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/mobile/ID26032322250002efCTPAY" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"id": "ID26032322250002efCTPAY",
"phone_number": "0999123456",
"amount": 5000,
"status": "completed",
"airtel_money_id": "MP260119.0848.B17208",
"category_flag": "LOAN_PAYMENT",
"customer_reference": "LOANREFE123",
"created_at": "2026-06-01 09:15:00"
}
}Download Receipts
/api/merchant/v1/transactions/bank/{id}/receipt
/api/merchant/v1/transactions/mobile/{id}/receipt
Returns a PDF receipt or receipt data for the specified transaction. Useful for generating downloadable receipts in your app.
# Bank receipt curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/bank/32aa1523-c105-472b-b0c5-fac0fcf6b6e2/receipt" \ -H "Authorization: Bearer YOUR_TOKEN" \ --output receipt.pdf # Mobile receipt curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/transactions/mobile/ID26032322250002efCTPAY/receipt" \ -H "Authorization: Bearer YOUR_TOKEN" \ --output receipt.pdf
Merchant Bank Accounts API
Manage settlement bank accounts for your merchant. You can list available banks, add accounts for settlement, and update or remove them.
List Available Banks
/api/merchant/v1/banks/available
Returns a list of banks supported for settlement. Use the bank identifier when creating a new bank account.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/banks/available" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": [
{ "id": 1, "name": "National Bank of Malawi", "code": "NBM" },
{ "id": 2, "name": "Standard Bank Malawi", "code": "STD" },
{ "id": 3, "name": "First Capital Bank", "code": "FCB" },
{ "id": 4, "name": "NBS Bank", "code": "NBS" }
]
}List Your Bank Accounts
/api/merchant/v1/bank-accounts
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/bank-accounts" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": [
{
"id": 1,
"bank_id": 1,
"bank_name": "National Bank of Malawi",
"bank_logo": "https://new-api.ctechpay.com/storage/banks/nbm.png",
"account_name": "Doe Enterprises Ltd",
"account_number": "1234567890",
"branch": "Lilongwe City Branch",
"phone_number": "0888123456",
"email": "finance@doeenterprises.com",
"created_at": "2026-05-01T10:00:00+00:00"
}
]
}Add Bank Account
/api/merchant/v1/bank-accounts
| Parameter | Type | Required | Description |
|---|---|---|---|
bank_id | integer | Required | Bank ID from the available banks list |
account_name | string | Required | Account holder name as registered at the bank |
account_number | string | Required | Bank account number |
branch | string | Required | Branch name |
phone_number | string | Required | Phone number associated with the bank account |
email | Optional | Email address associated with the bank account |
cURL
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/bank-accounts" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bank_id": 1,
"account_name": "Doe Enterprises Ltd",
"account_number": "1234567890",
"branch": "Lilongwe City Branch",
"phone_number": "0888123456",
"email": "finance@doeenterprises.com"
}'PHP (Guzzle)
$response = $client->post('https://new-api.ctechpay.com/api/merchant/v1/bank-accounts', [ 'headers' => ['Authorization' => 'Bearer ' . $token], 'json' => [ 'bank_id' => 1, 'account_name' => 'Doe Enterprises Ltd', 'account_number' => '1234567890', 'branch' => 'Lilongwe City Branch', 'phone_number' => '0888123456', 'email' => 'finance@doeenterprises.com', ], ]);
Update Bank Account
/api/merchant/v1/bank-accounts/{id}
Update an existing bank account. Send only the fields you want to change. Updatable fields: bank_id, account_number, account_name, branch, phone_number, email.
curl -X PUT "https://new-api.ctechpay.com/api/merchant/v1/bank-accounts/1" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"branch": "Area 18 Branch", "phone_number": "0999654321", "email": "updated@example.com"}'Delete Bank Account
/api/merchant/v1/bank-accounts/{id}
curl -X DELETE "https://new-api.ctechpay.com/api/merchant/v1/bank-accounts/1" \ -H "Authorization: Bearer YOUR_TOKEN"
Get a Single Bank Account
/api/merchant/v1/bank-accounts/{id}
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/bank-accounts/1" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Merchant Settlements API
Check your settlement balance and request fund transfers to your registered bank account. All endpoints require Bearer token authentication.
Get Settlement Balance
/api/merchant/v1/settlements/balance
Returns your current available balance that can be withdrawn/settled to your bank account.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/settlements/balance" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"available_balance": 850000.00,
"currency": "MWK",
"service_charge_pct": 5,
"categories": [
{
"id": 2,
"name": "Loan Application",
"flag": "LOAN_APPLICATION",
"charge_type": "fixed",
"available_balance": 4170000.00
}
]
}
}Get Settlement Options
/api/merchant/v1/settlements/options
Returns the same category-aware settlement data used by the merchant portal, including bank accounts, category balances, charge type and transaction counts.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/settlements/options" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Preview Settlement Fee
/api/merchant/v1/settlements/preview
Preview the available balance, estimated fee and expected payout before submitting the settlement request. Send values as JSON with POST, or as query parameters with GET when testing quickly.
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/settlements/preview" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settlement_type": "full",
"category_flag": "LOAN_APPLICATION"
}'curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/settlements/preview?settlement_type=full&category_flag=LOAN_APPLICATION" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
List Settlements
/api/merchant/v1/settlements
Returns a paginated history of all settlement requests and their statuses.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/settlements?page=1" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"settlements": [
{
"id": 12,
"amount": 200000,
"status": "completed",
"bank_account": "National Bank - 1234567890",
"requested_at": "2026-05-20 08:00:00",
"settled_at": "2026-05-21 10:30:00"
}
],
"total": 5,
"current_page": 1
}
}Request a Settlement
/api/merchant/v1/settlements
Submit a settlement request. Funds will be transferred to your designated bank account after processing. Use full to settle your entire available balance or selected category balance, or custom to settle a specific amount.
| Parameter | Type | Required | Description |
|---|---|---|---|
settlement_type | string | Required | Type of settlement: full (entire balance) or custom (specific amount) |
merchant_bank_id | integer | Required | ID of your bank account to settle to (from GET /bank-accounts) |
payment_category_id | integer | Optional | Request settlement for a specific payment category |
category_flag | string | Optional | Alternative to payment_category_id, for example LOAN_APPLICATION |
amount | numeric | Required for custom | Amount to settle in MWK. For full, omit this to settle the full selected scope balance. |
transactions | numeric | Optional | Number of transactions to include in this settlement |
cURL
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/settlements" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settlement_type": "full",
"merchant_bank_id": 1,
"category_flag": "LOAN_APPLICATION"
}'PHP (Guzzle)
$response = $client->post('https://new-api.ctechpay.com/api/merchant/v1/settlements', [ 'headers' => ['Authorization' => 'Bearer ' . $token], 'json' => [ 'settlement_type' => 'custom', 'merchant_bank_id' => 1, 'amount' => 200000, 'transactions' => 15, ], ]);
Python (requests)
import requests
headers = {'Authorization': 'Bearer YOUR_TOKEN', 'Accept': 'application/json'}
payload = {
'settlement_type': 'custom',
'merchant_bank_id': 1,
'amount': 200000,
'transactions': 15,
}
r = requests.post('https://new-api.ctechpay.com/api/merchant/v1/settlements',
json=payload, headers=headers)
print(r.json())// Settlement Request Response
{
"success": true,
"message": "Settlement requested successfully.",
"data": {
"id": 28,
"settlement_ref": "SETTLE_6a1e92197844b",
"amount": 8378.05,
"original_amount": 8819,
"tax_amount": 440.95,
"status": "pending",
"transactions": null,
"created_at": "2026-06-02T10:19:37+02:00",
"bank_account": {
"id": 3,
"bank_name": "National Bank of Malawi",
"account_number": "100xxxxxxxx",
"account_name": "James Doe"
},
"receipt": null,
"approved_at": null
}
}Get Settlement Detail
/api/merchant/v1/settlements/{id}
Returns full details of a specific settlement by its ID.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/settlements/12" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Settlement requests are typically processed within 1 business day. Ensure your bank account details are correct and verified before submitting. The amount must not exceed your available balance.
Merchant Reports API
Access detailed analytics and reports to gain insights into your payment activity, revenue trends, transaction health, and more.
Reports Overview
/api/merchant/v1/reports/overview
Returns a high-level summary of your merchant activity: total revenue, transaction count, success rates, settlement totals, and invoice counts. Supports optional date range filtering.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
from | date | Optional | Filter from date (YYYY-MM-DD) |
to | date | Optional | Filter to date (YYYY-MM-DD) |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/reports/overview?from=2026-06-01&to=2026-06-30" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"total_transactions": 1042,
"successful_transactions": 970,
"failed_transactions": 52,
"reversed_transactions": 20,
"total_revenue": 12500000.00,
"total_settlements": 9800000.00,
"pending_settlements": 450000.00,
"total_invoices": 84,
"paid_invoices": 71,
"unpaid_invoices": 9
}
}Monthly Revenue
/api/merchant/v1/reports/monthly-revenue
Returns month-by-month revenue data split by bank and mobile. Perfect for rendering charts and tracking revenue growth.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
months | integer | Optional | Number of past months to include (default: 12, max: 24) |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/reports/monthly-revenue?months=6" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"labels": ["Jan 2026", "Feb 2026", "Mar 2026", "Apr 2026", "May 2026", "Jun 2026"],
"bank": [320000, 410000, 580000, 490000, 620000, 450000],
"mobile": [300000, 330000, 400000, 380000, 480000, 400000],
"combined": [620000, 740000, 980000, 870000, 1100000, 850000]
}
}Transaction Status Distribution
/api/merchant/v1/reports/status-distribution
Returns a breakdown of transactions by status (completed, failed, pending). Useful for donut/pie charts.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/reports/status-distribution" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"completed": { "count": 970, "percentage": 93.1 },
"failed": { "count": 52, "percentage": 5.0 },
"pending": { "count": 20, "percentage": 1.9 }
}
}Settlements Report
/api/merchant/v1/reports/settlements
Returns a summarized settlements report with totals and history suitable for financial reconciliation. Supports filtering by status and date range.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by status: pending or completed |
from | date | Optional | Filter from date (YYYY-MM-DD) |
to | date | Optional | Filter to date (YYYY-MM-DD) |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/reports/settlements?status=completed&from=2026-01-01" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Invoices Report
/api/merchant/v1/reports/invoices
Returns an aggregate report of all invoices with totals by status and revenue from invoiced payments. Supports filtering.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by invoice status: PURCHASED, PENDING, STARTED, FAILED |
from | date | Optional | Filter from date (YYYY-MM-DD) |
to | date | Optional | Filter to date (YYYY-MM-DD) |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/reports/invoices?from=2026-06-01&to=2026-06-30" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Transactions Report
/api/merchant/v1/reports/transactions
Returns a detailed combined transactions report (bank + mobile). Supports optional date-range and status filtering for export and reconciliation.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
from | date | Optional | Report start date (YYYY-MM-DD) |
to | date | Optional | Report end date (YYYY-MM-DD) |
status | string | Optional | Filter by transaction status (e.g. PURCHASED, completed, failed) |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/reports/transactions?from=2026-06-01&to=2026-06-30&status=PURCHASED" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
PHP (Guzzle) — Reports example with date range
$response = $client->get('https://new-api.ctechpay.com/api/merchant/v1/reports/transactions', [ 'headers' => ['Authorization' => 'Bearer ' . $token], 'query' => [ 'from' => '2026-06-01', 'to' => '2026-06-30', 'status' => 'PURCHASED', ], ]); $report = json_decode($response->getBody(), true);
Merchant Notifications API
Manage in-app notifications for your merchant account — view alerts, mark them as read, and clean up old ones.
Get Unread Notification Count
/api/merchant/v1/notifications/unread-count
Returns the number of unread notifications. Great for notification badges in mobile/web apps.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/notifications/unread-count" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"count": 5
}List All Notifications
/api/merchant/v1/notifications
Returns all notifications for the merchant, paginated. Supports filtering by read/unread status.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
read | boolean | Optional | Filter by read status: true for read, false for unread only |
per_page | integer | Optional | Results per page (max 100, default 20) |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/notifications?page=1" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"unread_count": 5,
"data": [
{
"id": 101,
"type": "payment_received",
"message": "A payment of MWK 10,000 has been received.",
"read": false,
"created_at": "2026-06-02T09:15:00+00:00"
},
{
"id": 100,
"type": "settlement_completed",
"message": "Your settlement of MWK 200,000 has been processed.",
"read": true,
"created_at": "2026-05-21T11:00:00+00:00"
}
],
"meta": { "total": 47, "per_page": 20, "current_page": 1, "last_page": 3 }
}Get a Notification
/api/merchant/v1/notifications/{id}
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/notifications/101" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Mark a Notification as Read
/api/merchant/v1/notifications/{id}/read
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/notifications/101/read" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"message": "Notification marked as read."
}Mark All Notifications as Read
/api/merchant/v1/notifications/read-all
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/notifications/read-all" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Delete All Read Notifications
/api/merchant/v1/notifications/delete-read
Permanently removes all notifications that have already been marked as read. Useful for inbox cleanup.
curl -X DELETE "https://new-api.ctechpay.com/api/merchant/v1/notifications/delete-read" \ -H "Authorization: Bearer YOUR_TOKEN"
Delete a Notification
/api/merchant/v1/notifications/{id}
curl -X DELETE "https://new-api.ctechpay.com/api/merchant/v1/notifications/101" \ -H "Authorization: Bearer YOUR_TOKEN"
Node.js (axios) — Notification flow example
const axios = require('axios');
const BASE = 'https://new-api.ctechpay.com/api/merchant/v1';
const headers = { Authorization: 'Bearer YOUR_TOKEN' };
// 1. Check unread badge count (response key is "count")
const { data: countData } = await axios.get(`${BASE}/notifications/unread-count`, { headers });
console.log('Unread:', countData.count);
// 2. Fetch unread notifications only
const { data: listData } = await axios.get(`${BASE}/notifications`, { headers, params: { read: false, per_page: 20 } });
listData.data.forEach(n => console.log(n.type, n.message));
// 3. Mark all as read
await axios.post(`${BASE}/notifications/read-all`, {}, { headers });Merchant Service Charge API
Retrieve the current service charge configuration for your merchant account. This is a read-only endpoint — charge configurations are managed by the CtechPay admin.
Get Service Charge
/api/merchant/v1/service-charge
Returns the merchant's service charge rate, exemption status, and per-category charge details.
cURL
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/service-charge" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
PHP (Guzzle)
$response = $client->get('https://new-api.ctechpay.com/api/merchant/v1/service-charge', [ 'headers' => ['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json'], ]); $charge = json_decode($response->getBody(), true);
Python (requests)
import requests
headers = {'Authorization': 'Bearer YOUR_TOKEN', 'Accept': 'application/json'}
r = requests.get('https://new-api.ctechpay.com/api/merchant/v1/service-charge', headers=headers)
print(r.json())Sample Response — Exempted Merchant
{
"success": true,
"data": {
"charge_decimal": 0,
"charge_percentage": 0,
"is_exempted": true,
"exempted_amount": 500.00,
"effective_charge": 0,
"note": "You are exempt from service charges."
}
}Sample Response — Non-Exempted Merchant
{
"success": true,
"data": {
"charge_decimal": 0.035,
"charge_percentage": 3.5,
"is_exempted": false,
"exempted_amount": null,
"effective_charge": 3.5,
"note": "A 3.5% service charge applies to settlements."
}
}Response Fields
| Field | Type | Description |
|---|---|---|
charge_decimal | float | Charge stored as a decimal (e.g. 0.035 = 3.5%) |
charge_percentage | float | Human-readable percentage (e.g. 3.5) |
is_exempted | boolean | Whether this merchant is exempt from percentage charges |
exempted_amount | float / null | Fixed exemption cap amount in MWK (null when not exempted) |
effective_charge | float | The actual charge applied: 0 if exempted, otherwise same as charge_percentage |
note | string | Human-readable description of the charge configuration |
Use this endpoint to display service charge information inside your merchant app so users always see the current applicable fees before initiating transactions.
Merchant Invoices API
Create, manage, and send invoices to customers directly through the CtechPay platform. Customers can pay invoices via Airtel Money. All endpoints require Bearer token authentication.
Invoice Summary
/api/merchant/v1/invoices/summary
Returns aggregate invoice statistics.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/invoices/summary" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": {
"total": 84,
"total_value": 5200000.00,
"paid": 71,
"paid_value": 4400000.00,
"pending": 9,
"pending_value": 620000.00,
"failed": 4,
"bank_count": 45,
"mobile_count": 39,
"conversion_rate": 84.5
}
}List Invoices
/api/merchant/v1/invoices
Returns a paginated list of all invoices. Filter by status, payment method, or date range.
| Query Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | Optional | Page number |
per_page | integer | Optional | Results per page (max 100, default 20) |
status | string | Optional | Filter by status: PENDING, STARTED, PURCHASED, FAILED |
payment_method | string | Optional | Filter by payment method: bank or mobile |
from | date | Optional | Filter from date (YYYY-MM-DD) |
to | date | Optional | Filter to date (YYYY-MM-DD) |
search | string | Optional | Search by reference number, email, first or last name |
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/invoices?page=1&status=PENDING&payment_method=mobile" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Sample Response
{
"success": true,
"data": [
{
"id": 42,
"reference_number": "INV-2026-0042",
"payment_method": "mobile",
"status": "PENDING",
"first_name": "Alice",
"last_name": "Banda",
"email": "alice@example.com",
"total_value": 75000.00,
"invoice_expiry_date": "2026-06-15",
"created_at": "2026-06-02T08:00:00+00:00"
}
],
"meta": { "total": 9, "per_page": 20, "current_page": 1, "last_page": 1 }
}Create Invoice
/api/merchant/v1/invoices
Create a new invoice. Set payment_method to bank to send a payment link via email (Standard Bank gateway), or mobile to send an Airtel Money payment link via email. An email notification is automatically sent to the customer.
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_method | string | Required | Payment method: bank or mobile |
firstName | string | Required | Customer's first name |
lastName | string | Required | Customer's last name |
email | Required | Customer's email address (invoice is sent here) | |
emailSubject | string | Required | Subject line for the invoice email |
invoiceExpiryDate | date | Required | Invoice expiry / due date (must be after today, YYYY-MM-DD) |
message | string | Required | Invoice message / description (max 1000 characters) |
items | array | Required | Array of line items (minimum 1). See item structure below. |
items[].description | string | Required | Line item description |
items[].quantity | integer | Required | Quantity (minimum 1) |
items[].totalPrice.value | numeric | Required | Unit price (minimum 0.01) |
items[].totalPrice.currencyCode | string | Required | 3-letter ISO currency code (e.g. MWK) |
totalValue | numeric | Required | Total invoice amount (minimum 0.01) |
paymentAttempts | integer | Optional | Maximum payment attempts allowed (minimum 1) |
redirectUrl | url | Optional | URL to redirect customer after payment (bank invoices) |
skipInvoiceCreatedEmailNotification | boolean | Optional | Skip sending the creation email notification |
cURL
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/invoices" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"payment_method": "mobile",
"firstName": "Alice",
"lastName": "Banda",
"email": "alice@example.com",
"emailSubject": "Invoice for Services Rendered",
"invoiceExpiryDate": "2026-06-20",
"message": "Please pay for the monthly subscription service.",
"totalValue": 75000,
"paymentAttempts": 3,
"items": [
{
"description": "Monthly subscription fee",
"quantity": 1,
"totalPrice": { "value": 75000, "currencyCode": "MWK" }
}
]
}'PHP (Guzzle)
$response = $client->post('https://new-api.ctechpay.com/api/merchant/v1/invoices', [ 'headers' => ['Authorization' => 'Bearer ' . $token], 'json' => [ 'payment_method' => 'mobile', 'firstName' => 'Alice', 'lastName' => 'Banda', 'email' => 'alice@example.com', 'emailSubject' => 'Invoice for Services Rendered', 'invoiceExpiryDate' => '2026-06-20', 'message' => 'Please pay for the monthly subscription service.', 'totalValue' => 75000, 'paymentAttempts' => 3, 'items' => [[ 'description' => 'Monthly subscription fee', 'quantity' => 1, 'totalPrice' => ['value' => 75000, 'currencyCode' => 'MWK'], ]], ], ]);
Python (requests)
import requests
headers = {'Authorization': 'Bearer YOUR_TOKEN', 'Accept': 'application/json'}
payload = {
'payment_method': 'mobile',
'firstName': 'Alice',
'lastName': 'Banda',
'email': 'alice@example.com',
'emailSubject': 'Invoice for Services Rendered',
'invoiceExpiryDate': '2026-06-20',
'message': 'Please pay for the monthly subscription service.',
'totalValue': 75000,
'paymentAttempts': 3,
'items': [
{'description': 'Monthly subscription fee', 'quantity': 1,
'totalPrice': {'value': 75000, 'currencyCode': 'MWK'}}
],
}
r = requests.post('https://new-api.ctechpay.com/api/merchant/v1/invoices',
json=payload, headers=headers)
print(r.json())Node.js (axios)
const { data } = await axios.post(
'https://new-api.ctechpay.com/api/merchant/v1/invoices',
{
payment_method: 'mobile',
firstName: 'Alice',
lastName: 'Banda',
email: 'alice@example.com',
emailSubject: 'Invoice for Services Rendered',
invoiceExpiryDate: '2026-06-20',
message: 'Please pay for the monthly subscription service.',
totalValue: 75000,
paymentAttempts: 3,
items: [
{ description: 'Monthly subscription fee', quantity: 1,
totalPrice: { value: 75000, currencyCode: 'MWK' } }
],
},
{ headers: { Authorization: 'Bearer YOUR_TOKEN' } }
);
console.log(data);// Invoice Created Response
{
"success": true,
"message": "Mobile invoice sent to alice@example.com. The customer will receive a payment link.",
"data": {
"id": 43,
"reference_number": "INV2026A1B2C3D4E5F6",
"payment_method": "mobile",
"status": "STARTED",
"first_name": "Alice",
"last_name": "Banda",
"email": "alice@example.com",
"email_subject": "Invoice for Services Rendered",
"total_value": 75000.00,
"message": "Please pay for the monthly subscription service.",
"invoice_expiry_date": "2026-06-20",
"payment_attempts": 3,
"payment_link": null,
"redirect_url": null,
"checked": false,
"created_at": "2026-06-02T09:00:00+00:00"
},
"pay_url": "https://new-api.ctechpay.com/invoice/mobile/pay/TOKEN123..."
}Get Invoice Details
/api/merchant/v1/invoices/{id}
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/invoices/43" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
Update Invoice
/api/merchant/v1/invoices/{id}
Update an existing invoice that is in PENDING or STARTED state. Only non-gateway fields can be changed. Cannot edit invoices already in PURCHASED or FAILED state.
| Parameter | Type | Required | Description |
|---|---|---|---|
emailSubject | string | Optional | Updated email subject line |
invoiceExpiryDate | date | Optional | Updated expiry date (must be after today) |
message | string | Optional | Updated invoice message (max 1000 characters) |
redirectUrl | url | Optional | Updated redirect URL after payment |
paymentAttempts | integer | Optional | Updated payment attempt limit (minimum 1) |
curl -X PUT "https://new-api.ctechpay.com/api/merchant/v1/invoices/43" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"invoiceExpiryDate": "2026-06-25",
"message": "Please complete your payment before the new expiry date.",
"paymentAttempts": 5
}'Delete Invoice
/api/merchant/v1/invoices/{id}
curl -X DELETE "https://new-api.ctechpay.com/api/merchant/v1/invoices/43" \ -H "Authorization: Bearer YOUR_TOKEN"
Resend Invoice
/api/merchant/v1/invoices/{id}/resend
Re-sends the invoice to the customer via the payment gateway. Bank invoices only — this calls the Standard Bank gateway resend API. Optionally update the email address or expiry date when resending.
| Parameter | Type | Required | Description |
|---|---|---|---|
email | Optional | Override the customer email for this resend | |
invoiceExpiryDate | date | Optional | Override the expiry date for this resend (must be after today) |
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/invoices/43/resend" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "newemail@example.com",
"invoiceExpiryDate": "2026-06-30"
}'The resend endpoint only works for invoices with payment_method: "bank". Mobile invoices cannot be resent via this endpoint.
Refresh Invoice Payment Status
/api/merchant/v1/invoices/{id}/refresh-status
Manually triggers a status refresh for the invoice. CtechPay will query the payment gateway for the latest payment status and update the invoice accordingly.
curl -X POST "https://new-api.ctechpay.com/api/merchant/v1/invoices/43/refresh-status" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Response
{
"success": true,
"message": "Invoice status refreshed.",
"data": {
"id": 43,
"status": "paid",
"paid_at": "2026-06-05 14:22:00"
}
}Get Invoice Mobile Payment Status
/api/merchant/v1/invoices/{id}/mobile-status
Returns the Airtel Money payment status for the invoice. Useful for real-time polling when waiting for the customer to approve a payment push notification.
curl -X GET "https://new-api.ctechpay.com/api/merchant/v1/invoices/43/mobile-status" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json"
// Response — Payment in progress { "success": true, "data": { "invoice_id": 43, "mobile_status": "pending", "airtel_transaction_id": "ID26032322250002efCTPAY", "message": "Awaiting customer approval on Airtel Money" } } // Response — Payment confirmed { "success": true, "data": { "invoice_id": 43, "mobile_status": "completed", "airtel_transaction_id": "ID26032322250002efCTPAY", "airtel_money_id": "MP260602.1422.B99123", "message": "Payment received" } }
1. Create invoice → customer receives an email with a payment link
2. Bank invoice: customer clicks the link in the email and pays via the Standard Bank card payment gateway → use refresh-status to check payment
3. Mobile invoice: customer clicks the link in the email, enters their Airtel Money number, and approves on their phone → poll mobile-status every 5–10 seconds
4. Invoice status becomes PURCHASED once payment is confirmed
5. Use resend (bank only) to resend the payment link if the customer didn't receive it
Invoice Response Fields
| Field | Type | Description |
|---|---|---|
id | integer | Unique invoice ID on the CtechPay platform |
reference_number | string | Human-readable invoice reference (e.g. INV2026A1B2C3) |
bank_invoice_ref | string / null | Gateway invoice reference (bank invoices only) |
bank_order_ref | string / null | Gateway order reference (bank invoices only) |
payment_method | string | bank or mobile |
status | string | PENDING, STARTED, PURCHASED (paid), or FAILED |
first_name | string | Customer's first name |
last_name | string | Customer's last name |
email | string | Customer's email address |
email_subject | string | Invoice email subject line |
total_value | float | Total invoice amount in MWK |
message | string | Invoice message / description |
invoice_expiry_date | date | Invoice expiry / due date |
payment_attempts | integer | Maximum payment attempts allowed |
payment_link | string / null | Hosted payment page URL (bank invoices only) |
redirect_url | string / null | Post-payment redirect URL if set |
checked | boolean | true when invoice has reached a final status |
pay_url | string / null | Mobile payment URL sent to customer (mobile invoices only) |
mobile_trans_id | string / null | Airtel Money transaction ID (mobile invoices only, after payment initiated) |
items | array | Line items (only in single-invoice detail responses) |
Support
Use backend token storage, validate status before fulfillment, and log request/response IDs for reconciliation.