TypeScript SDK
The Papi TypeScript SDK (@ibonia/papi-api-client) provides a typed client for the Papi payment API. It handles HTTP communication, serialization, and model types so you can focus on your business logic. It runs on Node.js 18 or later.
Installation
The SDK is hosted on the Ibonia package repository. Add the registry to your .npmrc, then install the package.
Registry
@ibonia:registry=https://package-repository.ibonia.com/repository/ibonia-npm/
Yarn
yarn add @ibonia/papi-api-client
npm
npm install @ibonia/papi-api-client
The SDK depends on axios (~1.7.9), which is installed automatically.
Core classes
| Export | Kind | Description |
|---|---|---|
Configuration | class | Client configuration. Holds the base path; create it once and share it. |
PaymentLinksApi | class | Methods for creating payment links and reading them back. |
PaymentsApi | class | Method for checking the status of a payment by Papi's reference. |
PaymentLinkRequest | type | Request body for creating a payment link. |
PaymentLinkResponse | type | Response returned after a payment link is created. |
PaymentLinkStatusResponse | type | A payment link read back by reference, with its status and the payment outcome. Same field names as PaymentResponse, except Papi's reference is papiPaymentReference. |
PaymentResponse | type | Payload sent by Papi to your notification endpoint. |
ErrorResponse | type | Error body returned by the API. |
Every method returns an AxiosResponse, and every Papi body is an envelope. The payload is therefore always response.data.data.
Usage
Create one Configuration and pass it to the API classes you need.
import { Configuration, PaymentLinksApi, PaymentsApi } from '@ibonia/papi-api-client';
const config = new Configuration({ basePath: 'https://app.papi.mg' });
const links = new PaymentLinksApi(config);
const payments = new PaymentsApi(config);
The fields amount, clientName, reference, and description are required. notificationUrl is optional but strongly recommended.
import { PaymentLinkRequest } from '@ibonia/papi-api-client';
const request: PaymentLinkRequest = {
amount: 15000,
clientName: 'Client Name',
reference: 'ORDER-123',
description: 'Payment for Order #123',
notificationUrl: 'https://yourapp.com/payment-notify',
validDuration: 2, // link expires after 2 hours
};
To restrict the link to one provider, use the provider enum:
import { PaymentLinkRequestProviderEnum } from '@ibonia/papi-api-client';
request.provider = PaymentLinkRequestProviderEnum.Mvola;
Call createPaymentLink with your API key (from the dashboard) and the request.
const res = await links.createPaymentLink(apiKey, request);
const link = res.data.data; // PaymentLinkResponse
const paymentUrl = link.paymentLink; // redirect the user here
const notifToken = link.notificationToken; // store this for verification
const myReference = link.paymentReference; // your own reference, echoed back
Redirect the customer to paymentUrl. After the payment, Papi will call your notificationUrl with the result.
Calling createPaymentLink again with the same reference while the link is still live returns that same link (same URL, same token), so a retry is safe. The same reference with a different amount or currency answers 409.
try {
const res = await links.createPaymentLink(apiKey, request);
} catch (err: any) {
const status = err.response?.status; // 409, 401, 400…
const message = err.response?.data?.error?.message;
console.error(status, message);
}
Expose a POST endpoint that reads a PaymentResponse body. Papi calls this endpoint after every payment attempt. Verify the notification before doing anything else.
import express from 'express';
import { PaymentResponse, PaymentResponsePaymentStatusEnum } from '@ibonia/papi-api-client';
const app = express();
app.use(express.json());
app.post('/payment-notify', (req, res) => {
const notification = req.body as PaymentResponse;
// Values stored when the link was created
const expectedToken = 'xyz789';
const expectedReference = 'ORDER-123';
const tokenMatches = notification.notificationToken === expectedToken;
const refMatches = notification.merchantPaymentReference === expectedReference;
if (!tokenMatches || !refMatches) {
return res.status(403).end();
}
switch (notification.paymentStatus) {
case PaymentResponsePaymentStatusEnum.Success:
// Mark the order as paid
break;
case PaymentResponsePaymentStatusEnum.Failed:
// Handle the failure
break;
case PaymentResponsePaymentStatusEnum.Pending:
// Wait for the final notification
break;
}
res.status(200).end();
});
A notification is sent once. If your endpoint was down or the call was lost, ask Papi directly: getPaymentLink reads the link back by your merchant reference (the reference you sent at creation, merchantPaymentReference in notifications) and returns the same outcome the notification would have carried.
import { PaymentLinkStatusResponseLinkStatusEnum } from '@ibonia/papi-api-client';
const res = await links.getPaymentLink(apiKey, 'ORDER-123');
const link = res.data.data; // PaymentLinkStatusResponse
switch (link.linkStatus) {
case PaymentLinkStatusResponseLinkStatusEnum.Paid:
confirmOrder(link.papiPaymentReference); // paymentStatus is SUCCESS
break;
case PaymentLinkStatusResponseLinkStatusEnum.Active:
// still payable; paymentStatus tells you about the last attempt
break;
case PaymentLinkStatusResponseLinkStatusEnum.Expired:
case PaymentLinkStatusResponseLinkStatusEnum.Disabled:
issueNewLinkOrCancel();
break;
}
getPaymentLink answers 404 when no link of your shop carries that reference, and 401 when the API key is wrong.
When you already hold Papi's reference for the payment — paymentReference on a notification, or papiPaymentReference on a link read back — you can ask for the payment itself. This call takes no API key.
import { PaymentResponsePaymentStatusEnum } from '@ibonia/papi-api-client';
const res = await payments.getPaymentStatus('mvola', papiPaymentReference);
const payment = res.data.data; // PaymentResponse
if (payment.paymentStatus === PaymentResponsePaymentStatusEnum.Success) {
confirmOrder(payment.merchantPaymentReference);
}
The first argument is the provider the payment went through, as a string: 'mvola', 'airtel-money', 'orange-money', or 'bred-card'. An unknown payment reference answers 400.
PaymentLinkRequest fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | ✓ | Payment amount (minimum 300 MGA). |
clientName | string | ✓ | Customer's full name. |
reference | string | ✓ | Your unique identifier for this payment. |
description | string | ✓ | Short payment description (max 255 chars). |
notificationUrl | string | ✗ | Endpoint that receives payment status notifications. Strongly recommended; without it, use getPaymentLink to read the outcome. |
payerEmail | string | ✗ | Customer's email address. |
payerPhone | string | ✗ | Customer's phone number. |
successUrl | string | ✗ | Redirect URL after a successful payment. |
failureUrl | string | ✗ | Redirect URL after a failed payment. |
validDuration | number | ✗ | Link validity in hours (default: 1). |
provider | PaymentLinkRequestProviderEnum | ✗ | Lock to one provider: Mvola, AirtelMoney, OrangeMoney, or Bred. |
displayCurrency | string | ✗ | Currency shown to the payer (MGA only, default MGA). |
isTestMode | boolean | ✗ | true to flag the transaction as a test in the dashboard. |
testReason | string | ✗ | Reason displayed in the dashboard when test mode is on. |
successUrl and failureUrl go together: send both or neither.
PaymentResponse fields
| Field | Type | Description |
|---|---|---|
paymentStatus | PaymentResponsePaymentStatusEnum | Success, Pending, or Failed. |
paymentMethod | string | Provider used: MVOLA, AIRTEL_MONEY, ORANGE_MONEY, BRED. |
currency | string | Always MGA. |
displayCurrency | string | The currency the payer saw on the form (always MGA today). |
amount | number | Amount paid. |
estimatedAmount | number | The amount expressed in displayCurrency (equal to amount while only MGA is supported). |
fee | number | Transaction fee deducted. |
clientName | string | Customer's name. |
description | string | Payment description. |
merchantPaymentReference | string | Your reference from the original request. |
paymentReference | string | Papi's reference for this payment (a UUID). Pass it to getPaymentStatus. |
notificationToken | string | Token from the original payment link response — use it to verify authenticity. |
message | string | Failure reason, when there is one. |
payerEmail | string | Customer email (if provided). |
payerPhone | string | Customer phone (if provided). |
PaymentLinkStatusResponse fields
| Field | Type | Description |
|---|---|---|
linkStatus | PaymentLinkStatusResponseLinkStatusEnum | Active, Expired, Paid, or Disabled. Paid wins over Disabled and Expired. |
paymentStatus | PaymentResponsePaymentStatusEnum | Success, Pending, or Failed — same values as the notification. null while nobody has attempted to pay. |
paymentMethod | string | Provider the payer went through. null until a payment attempt exists. |
currency | string | Always MGA. |
displayCurrency | string | Currency shown to the payer. |
amount | number | Amount of the link. |
clientName | string | Customer's name. |
description | string | Payment description. |
merchantPaymentReference | string | Your reference from the original request. |
papiPaymentReference | string | Papi's reference for the payment attempt (the notification's paymentReference). null until one exists. |
notificationToken | string | Token returned when the link was created. |
message | string | Failure reason of the payment attempt, when there is one. |
payerEmail | string | Customer email (if provided). |
payerPhone | string | Customer phone (if provided). |
paymentLink | string | The payment URL. |
shortLink | string | Short form of the payment URL, when generated. |
linkCreationDateTime | number | Creation time, epoch milliseconds. |
linkExpirationDateTime | number | Expiry time, epoch milliseconds. |
isTestMode | boolean | Whether the link was flagged as a test. |
Full example (Express)
The example below shows a complete Express application that creates a payment link and handles the callback notification.
import express from 'express';
import {
Configuration,
PaymentLinksApi,
PaymentLinkRequest,
PaymentResponse,
PaymentResponsePaymentStatusEnum,
} from '@ibonia/papi-api-client';
const API_KEY = process.env.PAPI_API_KEY!;
const APP_DOMAIN = process.env.APP_DOMAIN!;
const config = new Configuration({ basePath: 'https://app.papi.mg' });
const links = new PaymentLinksApi(config);
const app = express();
app.use(express.json());
app.post('/checkout', async (req, res) => {
const { amount, reference, clientName, email, phone } = req.body;
const request: PaymentLinkRequest = {
amount,
clientName,
reference,
description: `Payment ${reference}`,
payerEmail: email,
payerPhone: phone,
notificationUrl: `${APP_DOMAIN}/payment-notify`,
validDuration: 2,
};
try {
const created = await links.createPaymentLink(API_KEY, request);
const link = created.data.data;
// Persist link.notificationToken alongside the order reference
// so you can verify it when the notification arrives.
await storeNotificationToken(reference, link.notificationToken);
res.json({ paymentLink: link.paymentLink });
} catch (err: any) {
res.status(err.response?.status ?? 500).json({
message: err.response?.data?.error?.message ?? 'Unexpected error',
});
}
});
app.post('/payment-notify', async (req, res) => {
const notification = req.body as PaymentResponse;
const storedToken = await lookupNotificationToken(
notification.merchantPaymentReference,
);
if (storedToken !== notification.notificationToken) {
return res.status(403).end();
}
if (notification.paymentStatus === PaymentResponsePaymentStatusEnum.Success) {
await confirmOrder(notification.merchantPaymentReference);
}
res.status(200).end();
});
app.listen(3000);
// ... storeNotificationToken, lookupNotificationToken and confirmOrder implementations