Instant Payment Notifications (IPNs) allow FinConnect-powered apps to receive real-time payment status updates from the provider. When a customer completes or fails a payment, the provider sends an HTTP request to your registered IPN URL so your server can update the order status immediately.
Currently, only PesaPal supports IPN registration via FinConnect. Calling registerIpn() on a ClickPesa SDK instance will throw an error: "IPN registration not supported by this provider".
Register an IPN URL
Call sdk.registerIpn() with your publicly accessible callback URL and the notification type (GET or POST). The method authenticates with PesaPal, registers the URL, and returns an ipnId.
// Register with GET notification type
const ipnId = await sdk.registerIpn('https://yourapp.com/ipn', 'GET');
// Or POST notification type
const ipnId = await sdk.registerIpn('https://yourapp.com/ipn', 'POST');
The returned ipnId is stored by PesaPal and used to identify your notification endpoint. You must pass it to every subsequent pay() call.
Set up your IPN handler
Your IPN handler must respond with HTTP 200 quickly. PesaPal sends OrderTrackingId, OrderMerchantReference, and OrderNotificationType as query parameters for GET-type notifications.
import express from 'express';
const app = express();
app.use(express.json());
// GET-based IPN handler
app.get('/ipn', (req, res) => {
const { OrderTrackingId, OrderMerchantReference, OrderNotificationType } = req.query;
// Verify payment status using OrderTrackingId
console.log('IPN received:', { OrderTrackingId, OrderMerchantReference });
res.status(200).send('OK');
});
Use a dedicated /ipn route and return 200 as quickly as possible. Perform any database updates or downstream API calls asynchronously so the response is not delayed.
Your IPN URL must be publicly accessible — PesaPal cannot reach localhost. Use a tunneling service such as ngrok during local development to expose your server to the internet.
Connect IPN registration to payments
The full flow is: register the IPN URL, capture the returned ipnId, then pass it to pay(). FinConnect merges the ipnId into the request payload as notification_id automatically.
import { FintechSDK } from 'finconnect';
const sdk = new FintechSDK({
provider: 'pesapal',
config: {
baseUrl: process.env.PESAPAL_BASE_URL!,
PESAPAL_CONSUMER_KEY: process.env.PESAPAL_CONSUMER_KEY!,
PESAPAL_CONSUMER_SECRET: process.env.PESAPAL_CONSUMER_SECRET!,
}
});
// Step 1: Register your IPN URL
const ipnId = await sdk.registerIpn('https://yourapp.com/ipn', 'GET');
// Step 2: Pass ipnId to pay()
const result = await sdk.pay({
id: 'ORD-2024-001',
currency: 'KES',
amount: 1500,
description: 'Order #2024-001',
callback_url: 'https://yourapp.com/callback',
billing_address: {
email_address: 'customer@example.com',
phone_number: '254712345678',
first_name: 'Jane',
last_name: 'Doe',
}
}, ipnId);
Azampay callback
Setting callback api
Azampay uses an HTTP POST Request to send back the transaction status. Finconnect helps to fetch the public key for verifying the signature to ensure the data sent isn’t tampered with.
Call sdk.handlecallback() to fetch the public key and verify the signature automatically.
import { FintechSDK } from 'finconnect';
const sdk = new FintechSDK({
provider: 'azampay',
config: {
baseUrl: process.env.AZAMPAY_BASE_URL || 'https://sandbox.azampay.co.tz',
AZAMPAY_APP_NAME: process.env.AZAMPAY_APP_NAME,
AZAMPAY_CONSUMER_KEY: process.env.AZAMPAY_CLIENT_ID,
AZAMPAY_CONSUMER_SECRET: process.env.AZAMPAY_API_KEY
}
});
app.post('/azampay/callback', async (req, res) => {
console.log('Received AzamPay Callback:', req.body);
try {
// This will fetch the public key and verify the signature automatically
const result = await sdk.handleCallback(req.body);
if (result.isValid) {
const transactionData = result.data;
console.log('✅ Payment Signature Verified!', transactionData);
// TODO: Update your database using transactionData
// Example: await updateOrder(transactionData.utilityref, transactionData.transactionstatus);
res.status(200).send('OK');
} else {
console.error('❌ Invalid signature received in callback');
res.status(400).send('Invalid Signature');
}
} catch (error: any) {
console.error('Callback processing error:', error.message);
res.status(500).send('Internal Server Error');
}
});