afripay
Version:
A TypeScript library to simplify integration with African payment processors (Orange Money, Wave, Paytech, Paydunya, etc).
487 lines • 16.9 kB
TypeScript
//#region src/processors/orange-money/types.d.ts
type OMMetadata = Record<string, string>;
type OMRequest = {
amount: {
unit: string;
value: number;
};
callbackCancelUrl: string;
callbackSuccessUrl: string;
code: number;
metadata?: OMMetadata;
name: string;
validity: number;
ipnUrl: string;
};
type OMResponse = {
deepLink: string;
deepLinks: {
MAXIT: string;
OM: string;
};
qrCode: string;
validity: number;
metadata?: OMMetadata;
shortLink: string;
qrId: string;
validFor: {
startDateTime: string;
endDateTime: string;
};
};
/**
* @internal
* This is the type of the webhook payload that Orange Money sends to your webhook URL.
* It is generic over the `metadata` field so you can customize it to any JSON-compatible type.
*
* @example
* ```ts
* type MyMetadata = { userId: number }
* type MyWebhookResponse = OMWebhookResponse<MyMetadata>
* ```
*/
type OMWebhookResponse<T extends OMMetadata> = {
amount: {
unit: 'XOF';
value: string;
};
partner: {
id: string;
idType: string;
};
customer: {
id: string;
idType: string;
};
reference: string;
type: string;
channel: string;
transactionId: string;
paymentMethod: string;
detail: string | null;
createdAt: string;
metadata?: T;
status: 'SUCCESS' | 'FAILED';
};
//#endregion
//#region src/processors/orange-money/payWithOrangeMoney.d.ts
declare const OM_PROD_BASE_URL = "https://api.orange-sonatel.com";
declare const OM_SANDBOX_BASE_URL = "https://api.sandbox.orange-sonatel.com";
declare const VALIDITY_LIMIT = 86400;
/**
* Creates an Orange Money checkout (QR or deeplink).
*
* @remarks
* Sends a request to Orange Money (Sonatel) to generate a payment intent that
* returns either:
*
* - a **MAXIT / Orange Money deeplink** (string), or
* - a **QR code** as a Base64 string (`qrCode`) that you need to manually parse
* to display to the user (e.g., `data:image/png;base64,${qrCode}`).
*
* This function chooses the base URL depending on `mode`:
* - `"test"` → {@link OM_SANDBOX_BASE_URL}
* - `"prod"` → {@link OM_PROD_BASE_URL}
*
* It requires the following environment variables:
* - `OM_CLIENT_ID`
* - `OM_CLIENT_SECRET`
*
* If `request.validity` exceeds {@link VALIDITY_LIMIT}, it is clamped to the
* maximum allowed.
*
* @param request - The Orange Money request payload. See {@link OMRequest}.
* @param mode - `"test"` for sandbox, `"prod"` for production. Defaults to `"test"`.
*
* @returns The Orange Money API response. See {@link OMResponse}.
*
* @throws AfriError
* - `missing_api_key` if required environment variables are not set
* - `request_failed` if the Orange Money API responds with a non-2xx status
*
* @example
* ```ts
* const res = await payWithOrangeMoney(
* {
* amount: { unit: 'XOF', value: 5000 },
* callbackCancelUrl: 'https://your.app/pay/cancel',
* callbackSuccessUrl: 'https://your.app/pay/success',
* ipnUrl: 'https://your.app/webhooks/ipn',
* code: 221, // country code
* name: 'Order #1234',
* validity: 600, // seconds
* metadata: { orderId: '1234' },
* },
* 'prod',
* )
*
* // If the response contains a deeplink (MAXIT / OM link), open it
* if (res.deeplink) {
* window.location.href = res.deeplink
* }
*
* // If the response contains a Base64 QR code, render it
* if (res.qrCode) {
* const src = `data:image/png;base64,${res.qrCode}`
* // <img src={src} alt="Pay with Orange Money" />
* }
* ```
*/
declare const payWithOrangeMoney: (request: OMRequest, mode?: "test" | "prod") => Promise<OMResponse>;
//#endregion
//#region src/processors/paydunya/types.d.ts
type CustomData = Record<string, string | Record<string, string>>;
type PaydunyaRequest = {
invoice: {
total_amount: number;
description: string;
};
store: {
name: string;
};
actions: {
cancel_url: string;
return_url: string;
callback_url: string;
};
custom_data?: CustomData;
};
type PaydunyaResponse = {
response_code: string;
response_text: string;
description: string;
token: string;
};
/**
* @internal
* This is the type of the webhook payload that PayDunya sends to your webhook URL.
* It is generic over the `custom_data` field so you can customize it to any JSON-compatible type.
*
* @example
* ```ts
* type MyCustomData = { userId: number }
* type MyWebhookResponse = PaydunyaWebhookResponse<MyCustomData>
* ```
*/
type PaydunyaWebhookResponse<T extends CustomData> = {
data: {
response_code: string;
response_text: string;
hash: string;
invoice: {
token: string;
total_amount: number;
description: string;
};
custom_data: T;
actions: {
cancel_url: string;
callback_url: string;
return_url: string;
};
mode: string;
status: string;
customer: {
name: string;
phone: string;
email: string;
};
receipt_url: string;
};
};
//#endregion
//#region src/processors/paydunya/payWithPaydunya.d.ts
declare const PAYDUNYA_TEST_URL = "https://app.paydunya.com/sandbox-api/v1/checkout-invoice/create";
declare const PAYDUNYA_PROD_URL = "https://app.paydunya.com/api/v1/checkout-invoice/create";
/**
* Create a PayDunya payment session (checkout / invoice).
*
* @remarks
* Sends a request to PayDunya to generate a hosted payment session. The API
* returns a short payload containing:
* - `response_code` — Gateway status code.
* - `response_text` — The redirect URL to the hosted checkout page.
* - `description` — Human-readable description of the transaction.
* - `token` — Unique token identifying the transaction (used for later verification).
*
* The runtime environment is selected via the `mode` parameter:
* - `"test"` → sandbox behavior
* - `"prod"` → live/production behavior
*
* Required environment variables:
* - `PAYDUNYA_PUBLIC_KEY`
* - `PAYDUNYA_PRIVATE_KEY`
* - `PAYDUNYA_TOKEN`
*
* @param request - The PayDunya request payload. See {@link PaydunyaRequest}.
* Typical fields include: amount, currency, label/description,
* success/cancel callbacks, IPN URL, and an optional
* idempotent `client_reference`.
* @param mode - `"test"` for sandbox or `"prod"` for production.
*
* @returns The PayDunya API response. See {@link PaydunyaResponse}:
* ```ts
* type PaydunyaResponse = {
* response_code: string // Status code from PayDunya
* response_text: string // Redirect URL to hosted checkout
* description: string // Description of the transaction
* token: string // Unique transaction token
* }
* ```
*
* @throws AfriError
* The function throws the following `AfriError` codes:
* - **`missing_api_key`**
* - Thrown when one of `PAYDUNYA_PUBLIC_KEY`, `PAYDUNYA_PRIVATE_KEY`, or
* `PAYDUNYA_TOKEN` is not set in the environment.
* - **`request_failed`**
* - Thrown when the PayDunya API responds with a non-2xx HTTP status.
* - The `details` (if available) contain the gateway’s error payload.
*
* @example
* ```ts
* const res = await payWithPaydunya(
* {
* amount: 5000,
* currency: 'XOF',
* description: 'Order #1234',
* success_url: 'https://app.example.com/pay/success',
* cancel_url: 'https://app.example.com/pay/cancel',
* ipn_url: 'https://app.example.com/api/ipn/paydunya',
* client_reference: 'ORDER_1234_2025-08-13',
* },
* 'prod',
* )
*
* // Redirect user to the hosted PayDunya checkout
* window.location.href = res.response_text
*
* // Optionally store the token for later verification
* saveTransactionToken(res.token)
* ```
*/
declare const payWithPaydunya: (request: PaydunyaRequest, mode?: "test" | "prod") => Promise<PaydunyaResponse>;
//#endregion
//#region src/processors/paytech/types.d.ts
declare const PAYMENT_METHODS: readonly ["Orange Money", "Orange Money CI", "Orange Money ML", "Mtn Money CI", "Moov Money CI", "Moov Money ML", "Wave", "Wave CI", "Wizall", "Carte Bancaire", "Emoney", "Tigo Cash", "Free Money", "Moov Money BJ", "Mtn Money BJ"];
type PAYMENT_METHODS = (typeof PAYMENT_METHODS)[number];
type PaytechRequest = {
item_name: string;
item_price: number;
ref_command: string;
command_name: string;
currency?: 'XOF' | 'EUR' | 'USD' | 'CAD' | 'GBP' | 'MAD';
env: 'test' | 'prod';
ipn_url?: string;
success_url?: string;
cancel_url?: string;
custom_field?: string;
target_payment?: PAYMENT_METHODS[];
refund_notif_url?: string;
};
type PaytechResponse = {
success: number;
token: string;
redirect_url: string;
redirectUrl: string;
};
/**
* @internal
* This is the type of the webhook payload that PayTech sends to your webhook URL.
* It is generic over the `custom_field` field
* By default, it is a string, but you can customize it to any JSON-compatible type.
*/
type PaytechWebhookResponse<T = string> = {
custom_field: T;
currency: string;
api_key_sha256: string;
api_secret_sha256: string;
type_event: string;
ref_command: string;
item_name: string;
item_price: string;
command_name: string;
token: string;
env: string;
payment_method: string;
client_phone: string;
};
//#endregion
//#region src/processors/paytech/payWithPaytech.d.ts
/**
* Create a PayTech payment session.
*
* @remarks
* Sends a request to PayTech to generate a payment session for the user. The
* response typically contains a **redirect URL** that you should send the user
* to in order to complete payment.
*
* The **environment** is selected via {@link PaytechRequest.env}:
* - `"test"` → sandbox behavior
* - `"prod"` → live/production behavior
*
* Required environment variables:
* - `PAYTECH_API_KEY`
* - `PAYTECH_API_SECRET`
*
* Notes:
* - `client_reference` should be **unique per attempt**. Reusing it may cause a
* **409 Conflict** from PayTech (duplicate reference).
*
* @param request - The PayTech request payload. At minimum, include:
* - `amount: number` — The amount to charge (in the currency’s minor unit if that’s how your integration is set up).
* - `currency: string` — e.g. `"XOF"`.
* - `success_url: string` — Where PayTech should redirect the user after a successful payment.
* - `error_url: string` — Where PayTech should redirect the user on failure/cancel.
* - `client_reference?: string` — Your internal idempotency/reference key (recommended to be unique).
* - `env: 'test' | 'prod'` — Selects sandbox vs production behavior.
* - (plus any other optional fields supported by your {@link PaytechRequest} type).
*
* @returns The PayTech API response with session details. See {@link PaytechResponse}.
* Implementations commonly expose a `redirect_url` you can send the user to.
*
* @throws AfriError
* The function throws the following `AfriError` codes:
* - **`missing_api_key`**
* - Thrown when `PAYTECH_API_KEY` or `PAYTECH_API_SECRET` is not set in the environment.
* - **`request_failed`**
* - Thrown when the PayTech API responds with a non‑2xx HTTP status.
* - If the status is **409**, the error message clarifies that there was a conflict,
* most likely because of a duplicated `ref_command`.
*
* @example
* ```ts
* const res = await payWithPaytech({
* amount: 5000,
* currency: 'XOF',
* success_url: 'https://app.example.com/pay/success',
* error_url: 'https://app.example.com/pay/error',
* client_reference: 'ORDER_1234_2025-08-13',
* env: 'prod',
* })
*
* // Typical usage: redirect the customer to PayTech’s hosted page
* if ((res as any).redirect_url) {
* // e.g., in a web context:
* // window.location.href = (res as any).redirect_url
* }
* ```
*/
declare const payWithPaytech: (request: PaytechRequest) => Promise<PaytechResponse>;
//#endregion
//#region src/processors/wave/types.d.ts
type WaveRequest = {
amount: number;
currency: 'XOF';
error_url: string;
success_url: string;
client_reference?: string;
};
type WaveResponse = {
id: string;
amount: string;
checkout_status: 'open' | 'complete' | 'expired';
client_reference: string;
currency: 'XOF';
error_url: string;
last_payment_error: string | null;
business_name: string;
payment_status: string;
transaction_id: string;
aggregated_merchant_id: string;
success_url: string;
wave_launch_url: string;
when_completed: string;
when_created: string;
when_expires: string;
};
type WaveWebhookResponse = {
id: string;
type: string;
data: {
id: string;
amount: string;
checkout_status: 'open' | 'complete' | 'expired';
client_reference: string;
currency: string;
error_url: string;
last_payment_error: string | null;
business_name: string;
payment_status: string;
transaction_id: string;
aggregated_merchant_id: string;
success_url: string;
wave_launch_url: string;
when_completed: string;
when_created: string;
when_expires: string;
};
};
//#endregion
//#region src/processors/wave/payWithWave.d.ts
declare const WAVE_BASE_URL = "https://api.wave.com";
/**
* Initiates a Wave checkout session to process a payment.
*
* @remarks
* This function sends a POST request to Wave's `/v1/checkout/sessions` endpoint.
* If `mode` is `'test'`, it will always charge 1 unit of the currency (for test purposes).
* In `'prod'` mode, it uses the actual `amount` provided in the `request` object.
*
* The `client_reference` optional field in the request is an arbitrary string that your application
* can use to tie this payment back to some entity on your side (for example, a user ID,
* an order ID, or any other reference you need). Wave will return this same value in the response,
* so you can match incoming webhooks or redirect callbacks to your internal records.
*
* When the checkout session is successfully created, Wave returns a `wave_launch_url` in the response.
* You should redirect the user’s browser to that URL so they can complete the payment flow.
*
* @param {WaveRequest} request
* - `amount`: The payment amount (in smallest currency unit) to process. Ignored if `mode` is `'test'`.
* - `currency`: The three-letter currency code (only XOF is supported at the moment).
* - `error_url`: The URL to which Wave will redirect the user if the payment fails.
* - `success_url`: The URL to which Wave will redirect the user if the payment succeeds.
* - `client_reference`: An arbitrary identifier (e.g., userId or orderId) that Wave will return
* in the response. This allows you to correlate Wave’s response with your own records.
* @param {'test' | 'prod'} [mode='test']
* - `'test'`: Creates a checkout session with a nominal amount (1 unit) for testing.
* - `'prod'`: Uses the actual `request.amount` when creating the checkout session.
*
* @returns {Promise<WaveResponse>}
* A promise that resolves to the `WaveResponse` object returned by Wave. Key fields include:
* - `wave_launch_url`: The URL where you must redirect the user to complete payment.
* - `client_reference`: Echoes back the same reference you sent, so you can match it on your end.
*
* @throws {AfriError}
* - If `process.env.WAVE_API_KEY` is not set, an error is thrown with the reason `'missing_api_key'`.
* - If the HTTP response from Wave is not OK (status code not in the 2xx range),
* an error with message with the reason `'request_failed'` is thrown.
*
* @example
* ```ts
* import { payWithWave } from './payments'
*
* // Somewhere in your server-side code (e.g., an Express handler):
* app.post('/create-payment', async (req, res) => {
* const waveRequest: WaveRequest = {
* amount: 5000, // e.g., $50.00 in cents
* currency: 'XOF',
* error_url: 'https://example.com/payment-error',
* success_url: 'https://example.com/payment-success',
* client_reference: 'user_1234'
* }
*
* try {
* const waveResponse = await payWithWave(waveRequest, 'prod')
* // Redirect user to Wave’s payment page:
* res.redirect(waveResponse.wave_launch_url)
* } catch (err) {
* console.error('Payment failed to initialize:', err)
* res.status(500).send('Unable to start payment.')
* }
* })
* ```
*/
declare const payWithWave: (request: WaveRequest, mode?: "test" | "prod") => Promise<WaveResponse>;
//#endregion
export { CustomData, OMMetadata, OMRequest, OMResponse, OMWebhookResponse, OM_PROD_BASE_URL, OM_SANDBOX_BASE_URL, PAYDUNYA_PROD_URL, PAYDUNYA_TEST_URL, PAYMENT_METHODS, PaydunyaRequest, PaydunyaResponse, PaydunyaWebhookResponse, PaytechRequest, PaytechResponse, PaytechWebhookResponse, VALIDITY_LIMIT, WAVE_BASE_URL, WaveRequest, WaveResponse, WaveWebhookResponse, payWithOrangeMoney, payWithPaydunya, payWithPaytech, payWithWave };