CtechPay Documentation Dashboard
Documentation CtechPay API Integration
Start Integrating

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.

Prerequisites

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

Security Warning

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.

PHP Composer

composer require ctechpay/ctechpay-php

View on Packagist

Node.js NPM

npm install @ctechpay/ctechpay-js

View on NPM

Python PIP

pip install ctechpay

View on PyPI

Flutter Mobile

flutter pub add ctechpay

View on pub.dev

WooCommerce Plugin

Plugins > Add New > Upload Plugin

Download WordPress ZIP

Recommended

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.

Mobile Security

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
WooCommerce Checkout

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

POST /api/v1/hosted/payment
ParameterTypeRequiredDescription
tokenstringRequiredService API token
amountnumericRequiredAmount in MWK
category_flagstringOptionalMerchant category flag
customer_referencestringOptionalYour own transaction reference
customer_messagestringOptionalMessage shown on the hosted page
customer_namestringOptionalCustomer name for your records
customer_emailemailOptionalCustomer email for your records
redirectUrlurlOptionalWhere to send the customer after a successful hosted payment
cancelUrlurlOptionalWhere 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

POST /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

Automation tip

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 Methodreference valueStatus 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

POST /api/v1/airtel/payment
ParameterTypeRequiredDescription
tokenstringRequiredService API token
amountnumericRequiredAmount in MWK
phonestringRequiredAirtel number (e.g. 0999123456)
category_flagstringOptionalMerchant category flag
customer_referencestringOptionalYour own defined reference for the transaction
customer_messagestringOptionalYour 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

FieldTypeDescription
statusstringTop-level outcome: success or error.
messagestringHuman-readable result of the initiation request.
data.data.transaction.idstringCtechPay transaction ID. Save this value for Steps 2 and 3.
data.data.transaction.statusstringAirtel network initiation status. Success. means push notification was dispatched.
data.status.response_codestringAirtel gateway response code. DP00800001006 means initiated successfully.
data.status.codestringHTTP-equivalent status code from Airtel gateway.
data.status.successbooleantrue if initiation was accepted by Airtel network.
data.status.result_codestringESB result code. ESB000010 indicates success.
data.status.messagestringESB outcome message (for example SUCCESS).
customer_referencestringCustomer reference provided during payment initiation.
customer_messagestringCustomer message provided during payment initiation.
Payment Flow

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

POST /api/v1/airtel/status
ParameterTypeRequiredDescription
trans_idstringRequiredTransaction 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

FieldTypeDescription
response_codestringAirtel gateway response code. DP00800001001 means success.
result_codestringESB result code. ESB000010 means transaction successful.
transaction_statusstringShort status code: TS = successful, TF = failed.
airtel_money_idstringAirtel Money reference ID for the transaction.
messagestringHuman-readable outcome message.
Polling Tip

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:

EndpointStatus CodeTypical CauseWhat to Check
/api/v1/airtel/payment422Invalid phone formatEnsure Malawian number format and valid length.
/api/v1/airtel/payment401Invalid or missing tokenConfirm your Service API token is correct and active.
/api/v1/airtel/status400Missing trans_idAlways pass the exact transaction ID returned during initiation.
/api/v1/airtel/status422Validation failureCheck 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

GET /api/v1/airtel/transaction/details
ParameterTypeRequiredDescription
transaction_idstringRequiredSame 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

FieldTypeDescription
statusstringAPI call outcome: success or error.
trans_idstringCtechPay transaction ID (echoed back).
airtel_money_idstringAirtel Money network reference. Keep this for reconciliation.
transaction_statusstringVerbose status: completed or failed.
phone_numberstringCustomer's phone number as registered on Airtel Money.
result_codestringESB result code (ESB000010 = success).
response_codestringAirtel gateway response code (DP00800001001 = success).
a_trans_statusstringShort Airtel status code: TS = successful, TF = failed.
messagestringHuman-readable outcome message from the Airtel network.

Card Payment API

Recommended flow: Create order - Redirect customer - Verify status/details.

Step 1 - Create Payment Order

POST /api/v1/orders
ParameterTypeRequiredDescription
tokenstringRequiredYour Service API token
amountnumericRequiredPayment amount in MWK
category_flagstringOptionalPayment category identifier
customer_referencestringOptionalYour own defined reference for the transaction
customer_messagestringOptionalYour own message/description for the transaction
merchantAttributesbooleanOptionalSet to true to enable custom merchant attributes.
redirectUrlurlOptionalURL to redirect after successful payment (requires merchantAttributes=true).
cancelUrlurlOptionalURL to redirect if payment is cancelled (requires merchantAttributes=true).
cancelTextstringOptionalCustom text for cancel button (requires merchantAttributes=true).
skipConfirmationPagebooleanOptionalSkip 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"
}
Payment Flow

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

Merchant Attributes

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.

GET /api/v1/orders/status
ParameterTypeRequiredDescription
orderRefstringYesThe order reference returned when creating the order
tokenstringYesYour 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.

GET /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

FieldTypeDescription
order_idstring (UUID)Unique order identifier on the CtechPay platform.
referencestring / nullMerchant-supplied reference, if provided at creation.
currencyCodestringISO 4217 currency code (always MWK for Malawi).
amountnumericPayment amount in MWK.
category_flagstring / nullCategory flag assigned to the order, if any.
batch_idinteger / nullSettlement batch ID, populated after settlement.
formattedAmountstringHuman-readable amount string (for example MWK 150000).
statusstringOrder status: PURCHASED, STARTED, FAILED, etc.
card_holderstringName of the cardholder as entered during payment.

Payment Categories

Organize and track your transactions by assigning them to different categories.

What are Payment 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:

  1. Log in to your CtechPay Dashboard
  2. Navigate to Settings -> Payment Categories
  3. Create a new category with a name and flag
  4. Use the category flag in your API requests

Use category_flag to segment transaction reports by business use case.

Financial

LOAN_APPLICATION_FEES
LOAN_RECOVERY
LOAN_DISBURSEMENT

E-commerce

PRODUCT_PURCHASE
SUBSCRIPTION_FEE
SHIPPING_CHARGES

Services

CONSULTATION_FEE
SERVICE_CHARGE
MEMBERSHIP_FEE

Error Handling

The API returns standard HTTP status codes and error messages to help you troubleshoot issues.

Common Status Codes

CodeStatusDescription
200OKRequest successful
400Bad RequestInvalid parameters or missing required fields
401UnauthorizedInvalid or missing API token
422Unprocessable EntityValidation error (check error details)
500Internal Server ErrorSomething 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

POST /api/merchant/v1/auth/login
ParameterTypeRequiredDescription
emailstringRequiredMerchant email address
passwordstringRequiredMerchant 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 storage

Python (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 requests

Success 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."
}
Token Security

• 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

EndpointMethodDescription
/api/merchant/v1/auth/logoutPOSTLogout and invalidate current token
/api/merchant/v1/auth/profileGETGet authenticated merchant profile
/api/merchant/v1/auth/change-passwordPOSTChange merchant password

Change Password — Request Body

ParameterTypeRequiredDescription
current_passwordstringRequiredThe merchant's current password
new_passwordstringRequiredNew password (minimum 8 characters)
new_password_confirmationstringRequiredMust 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"
  }'
Using the Token

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

GET /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."
}
Authentication Required

This endpoint requires a Bearer token. See Merchant Authentication section to obtain your token via the login endpoint.

Response Fields Explained

FieldTypeDescription
account_infoobjectPersonal account details (name, email, phone, username, status)
organizationobjectBusiness/company information
service_chargeobjectService charge configuration and exemption details
service_charge.is_exemptedbooleanWhether merchant is exempt from percentage charges
exempted_categoriesarray|nullCategories with fixed charge amounts (only if exempted)
percentage_categoriesarray|nullCategories with percentage charges (only if not exempted)
applicationobject|nullApproved application details with API credentials
application.application_keystringApplication key used for programmatic access and webhook signature verification
Use Cases

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

Where to configure webhooks

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

ItemDescription
payment_initiatedSent immediately after CtechPay has created and submitted the payment request.
payment_status_updatedSent when CtechPay receives or confirms a final/updated payment status.
Success responseYour endpoint must return any HTTP 2xx response.
RetriesFailed webhook deliveries are retried up to 5 times with backoff.
IdempotencyStore and deduplicate by webhook_event_id. You can also deduplicate payment records by transaction_id or order_reference.

Headers

HeaderDescription
CtechPay-EventWebhook event name, for example payment_initiated.
CtechPay-DeliveryUnique delivery/event ID. This matches webhook_event_id in the JSON payload.
CtechPay-TimestampUnix timestamp used when signing the payload.
CtechPay-SignatureSignature 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,
]);
Respond quickly

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.

Authentication

All endpoints in this section require the Authorization: Bearer {token} header obtained from the Merchant Login endpoint.

List All Transactions

GET /api/merchant/v1/transactions

Returns a paginated list of all transactions (both bank and mobile) for the authenticated merchant. Supports optional query filters.

Query ParameterTypeRequiredDescription
pageintegerOptionalPage number for pagination
per_pageintegerOptionalResults per page (max 100)
statusstringOptionalFilter by status (e.g. PURCHASED, completed, failed)
categorystringOptionalFilter by category flag (e.g. LOAN_PAYMENT). Use uncategorized for transactions with no category
fromdateOptionalFilter from date (YYYY-MM-DD)
todateOptionalFilter to date (YYYY-MM-DD)
searchstringOptionalSearch 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

GET /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

GET /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

GET /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

GET /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

GET /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

GET /api/merchant/v1/transactions/bank/{id}/receipt
GET /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

GET /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

GET /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

POST /api/merchant/v1/bank-accounts
ParameterTypeRequiredDescription
bank_idintegerRequiredBank ID from the available banks list
account_namestringRequiredAccount holder name as registered at the bank
account_numberstringRequiredBank account number
branchstringRequiredBranch name
phone_numberstringRequiredPhone number associated with the bank account
emailemailOptionalEmail 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

PUT /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

DELETE /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

GET /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

GET /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

GET /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

GET/POST /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

GET /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

POST /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.

ParameterTypeRequiredDescription
settlement_typestringRequiredType of settlement: full (entire balance) or custom (specific amount)
merchant_bank_idintegerRequiredID of your bank account to settle to (from GET /bank-accounts)
payment_category_idintegerOptionalRequest settlement for a specific payment category
category_flagstringOptionalAlternative to payment_category_id, for example LOAN_APPLICATION
amountnumericRequired for customAmount to settle in MWK. For full, omit this to settle the full selected scope balance.
transactionsnumericOptionalNumber 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

GET /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 Processing

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

GET /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 ParameterTypeRequiredDescription
fromdateOptionalFilter from date (YYYY-MM-DD)
todateOptionalFilter 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

GET /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 ParameterTypeRequiredDescription
monthsintegerOptionalNumber 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

GET /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

GET /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 ParameterTypeRequiredDescription
statusstringOptionalFilter by status: pending or completed
fromdateOptionalFilter from date (YYYY-MM-DD)
todateOptionalFilter 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

GET /api/merchant/v1/reports/invoices

Returns an aggregate report of all invoices with totals by status and revenue from invoiced payments. Supports filtering.

Query ParameterTypeRequiredDescription
statusstringOptionalFilter by invoice status: PURCHASED, PENDING, STARTED, FAILED
fromdateOptionalFilter from date (YYYY-MM-DD)
todateOptionalFilter 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

GET /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 ParameterTypeRequiredDescription
fromdateOptionalReport start date (YYYY-MM-DD)
todateOptionalReport end date (YYYY-MM-DD)
statusstringOptionalFilter 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

GET /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

GET /api/merchant/v1/notifications

Returns all notifications for the merchant, paginated. Supports filtering by read/unread status.

Query ParameterTypeRequiredDescription
readbooleanOptionalFilter by read status: true for read, false for unread only
per_pageintegerOptionalResults 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

GET /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

POST /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

POST /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

DELETE /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

DELETE /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

GET /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

FieldTypeDescription
charge_decimalfloatCharge stored as a decimal (e.g. 0.035 = 3.5%)
charge_percentagefloatHuman-readable percentage (e.g. 3.5)
is_exemptedbooleanWhether this merchant is exempt from percentage charges
exempted_amountfloat / nullFixed exemption cap amount in MWK (null when not exempted)
effective_chargefloatThe actual charge applied: 0 if exempted, otherwise same as charge_percentage
notestringHuman-readable description of the charge configuration
Service Charge Use Case

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

GET /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

GET /api/merchant/v1/invoices

Returns a paginated list of all invoices. Filter by status, payment method, or date range.

Query ParameterTypeRequiredDescription
pageintegerOptionalPage number
per_pageintegerOptionalResults per page (max 100, default 20)
statusstringOptionalFilter by status: PENDING, STARTED, PURCHASED, FAILED
payment_methodstringOptionalFilter by payment method: bank or mobile
fromdateOptionalFilter from date (YYYY-MM-DD)
todateOptionalFilter to date (YYYY-MM-DD)
searchstringOptionalSearch 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

POST /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.

ParameterTypeRequiredDescription
payment_methodstringRequiredPayment method: bank or mobile
firstNamestringRequiredCustomer's first name
lastNamestringRequiredCustomer's last name
emailemailRequiredCustomer's email address (invoice is sent here)
emailSubjectstringRequiredSubject line for the invoice email
invoiceExpiryDatedateRequiredInvoice expiry / due date (must be after today, YYYY-MM-DD)
messagestringRequiredInvoice message / description (max 1000 characters)
itemsarrayRequiredArray of line items (minimum 1). See item structure below.
items[].descriptionstringRequiredLine item description
items[].quantityintegerRequiredQuantity (minimum 1)
items[].totalPrice.valuenumericRequiredUnit price (minimum 0.01)
items[].totalPrice.currencyCodestringRequired3-letter ISO currency code (e.g. MWK)
totalValuenumericRequiredTotal invoice amount (minimum 0.01)
paymentAttemptsintegerOptionalMaximum payment attempts allowed (minimum 1)
redirectUrlurlOptionalURL to redirect customer after payment (bank invoices)
skipInvoiceCreatedEmailNotificationbooleanOptionalSkip 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

GET /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

PUT /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.

ParameterTypeRequiredDescription
emailSubjectstringOptionalUpdated email subject line
invoiceExpiryDatedateOptionalUpdated expiry date (must be after today)
messagestringOptionalUpdated invoice message (max 1000 characters)
redirectUrlurlOptionalUpdated redirect URL after payment
paymentAttemptsintegerOptionalUpdated 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

DELETE /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

POST /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.

ParameterTypeRequiredDescription
emailemailOptionalOverride the customer email for this resend
invoiceExpiryDatedateOptionalOverride 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"
  }'
Bank Invoices Only

The resend endpoint only works for invoices with payment_method: "bank". Mobile invoices cannot be resent via this endpoint.

Refresh Invoice Payment Status

POST /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

GET /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"
  }
}
Invoice Lifecycle

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

FieldTypeDescription
idintegerUnique invoice ID on the CtechPay platform
reference_numberstringHuman-readable invoice reference (e.g. INV2026A1B2C3)
bank_invoice_refstring / nullGateway invoice reference (bank invoices only)
bank_order_refstring / nullGateway order reference (bank invoices only)
payment_methodstringbank or mobile
statusstringPENDING, STARTED, PURCHASED (paid), or FAILED
first_namestringCustomer's first name
last_namestringCustomer's last name
emailstringCustomer's email address
email_subjectstringInvoice email subject line
total_valuefloatTotal invoice amount in MWK
messagestringInvoice message / description
invoice_expiry_datedateInvoice expiry / due date
payment_attemptsintegerMaximum payment attempts allowed
payment_linkstring / nullHosted payment page URL (bank invoices only)
redirect_urlstring / nullPost-payment redirect URL if set
checkedbooleantrue when invoice has reached a final status
pay_urlstring / nullMobile payment URL sent to customer (mobile invoices only)
mobile_trans_idstring / nullAirtel Money transaction ID (mobile invoices only, after payment initiated)
itemsarrayLine items (only in single-invoice detail responses)

Support

Integration Checklist

Use backend token storage, validate status before fulfillment, and log request/response IDs for reconciliation.