@morapay/moolre
v0.1.1
Published
A wrapper for Moolre payments, SMS, and bank rails (Ghana & Nigeria)
Readme
@morapay/moolre
A wrapper for Moolre payments, SMS, bank collection, and payout transfers in Ghana and Nigeria.
Most Moolre integrations fail because of subtle API details — wrong channel ids (collection 13 vs transfer 1), missing OTP follow-up (TP14 → TP17 → TR099), incorrect phone formats, or mixing sandbox/live credentials. This SDK handles those details so you pass simple props and get predictable results.
Before you install — SMS setup (Moolre dashboard)
SMS is not enabled by default on a Moolre account. Before using moolre.sms, complete these steps in your Moolre dashboard:
- Create an SMS service on your Moolre account (VAS / SMS product).
- Top up SMS credits — at least ~5 GHS (or less if Moolre allows a smaller minimum on your plan). Without credits, sends will fail.
- Register a Sender ID (e.g.
YourBrand) and wait for approval if required. - Copy your SMS API key (
X-API-VASKEY) and approved Sender ID into env:
MOOLRE_SMS_API_KEY=your_vas_key
MOOLRE_SMS_SENDER_ID=YourBrandSMS always calls the live Moolre SMS API (
api.moolre.com), even whenMOOLRE_MODE=sandboxfor payments.
Modes — live vs sandbox
Set MOOLRE_MODE or MOOLRE_ENV (or pass mode to the client):
| Value | API base | When to use | Keys required |
|-------|----------|-------------|---------------|
| live (default) | https://api.moolre.com | Production payments, payment links, bank accounts, payouts, status | MOOLRE_PRIVATE_API_KEY, MOOLRE_PUBLIC_API_KEY, MOOLRE_ACCOUNT_NUMBER |
| sandbox (alias: test) | https://sandbox.moolre.com | Safe integration testing without real money | MOOLRE_USERNAME + MOOLRE_ACCOUNT_NUMBER (or MOOLRE_SANDBOX_ACCOUNT_NUMBER) |
# Production (default)
MOOLRE_MODE=live
# Sandbox
MOOLRE_MODE=sandboximport { Moolre } from "@morapay/moolre";
const live = Moolre.fromEnv(); // MOOLRE_MODE unset → live
const sandbox = live.sandbox(); // same creds, sandbox base URL
const explicit = live.withMode("live"); // switch back to productionMorapay CLI equivalents (internal test scripts — same APIs this SDK wraps):
| Script | SDK equivalent | Typical mode |
|--------|----------------|--------------|
| pnpm moolre:sms-test | moolre.sms.send() | live (SMS API only) |
| pnpm moolre:payment-link-test | moolre.links.create() | live |
| pnpm moolre:bank-test | moolre.virtualAccounts.create() + moolre.banks.list() | live |
| pnpm moolre:payment-test | moolre.payments.initiate() / submitOtp() | live |
| pnpm moolre:transfer-test | moolre.transfers.initiate() | live |
Install
Works with npm, pnpm, yarn, and bun:
npm install @morapay/moolrepnpm add @morapay/moolreyarn add @morapay/moolrebun add @morapay/moolreRequires Node.js 18+ (Bun 1.0+ also supported).
Quick start
1. Set environment variables
# Required
MOOLRE_USERNAME=your_moolre_username
MOOLRE_ACCOUNT_NUMBER=your_wallet_account_number
# Live transact (MoMo collection, transfers, validate)
MOOLRE_PRIVATE_API_KEY=your_private_key
# Live payment links, status, virtual bank accounts
MOOLRE_PUBLIC_API_KEY=your_public_key
# Sandbox (integration testing — sandbox.moolre.com)
MOOLRE_MODE=sandbox
MOOLRE_SANDBOX_ACCOUNT_NUMBER=your_sandbox_account
# SMS (optional — always hits live SMS API)
MOOLRE_SMS_API_KEY=your_vas_key
MOOLRE_SMS_SENDER_ID=YourBrand
# Callbacks (optional)
MOOLRE_PAYMENT_CALLBACK_URL=https://yourapp.com/webhooks/moolre/payment
MOOLRE_TRANSFER_CALLBACK_URL=https://yourapp.com/webhooks/moolre/transfer2. Create a client
import { Moolre } from "@morapay/moolre";
// From env (recommended)
const moolre = Moolre.fromEnv();
// Or explicit config
const moolreLive = new Moolre({
username: process.env.MOOLRE_USERNAME!,
accountNumber: process.env.MOOLRE_ACCOUNT_NUMBER!,
privateKey: process.env.MOOLRE_PRIVATE_API_KEY!,
publicKey: process.env.MOOLRE_PUBLIC_API_KEY!,
mode: "live",
});3. Switch sandbox ↔ live
const sandbox = moolre.sandbox(); // MOOLRE_MODE=sandbox or .withMode("sandbox")
const live = moolre.live(); // MOOLRE_MODE=live (production)
// Or
const client = moolre.withMode("sandbox");Bank flows
The SDK covers three bank-related flows:
1. Inbound collection — virtual bank accounts
Customer receives a dedicated account number and pays by bank transfer (Ghana / Nigeria):
const banks = await moolre.banks.list({ country: "ghana" });
const account = await moolre.virtualAccounts.create({
currency: "GHS",
email: "[email protected]",
firstName: "Jeffery",
lastName: "Mintah",
phone: "0244123456",
amount: 10, // optional expected amount
});
console.log(account.bankname, account.accountno, account.accountname);
// Customer transfers to accountno for the exact amountConfirm payment via webhook or moolre.transactions.getStatus({ reference: account.uref }).
2. Hosted payment link (Web POS)
Card / bank checkout on Moolre's hosted page — no OTP step on your side:
const link = await moolre.links.create({
amount: 5,
currency: "GHS",
email: "[email protected]",
redirectUrl: "https://yourapp.com/success",
});
console.log(link.authorizationUrl); // open in browser3. Outbound bank payouts
Send to Nigeria or Ghana bank accounts:
const banks = await moolre.banks.list({ country: "nigeria" });
const name = await moolre.validate.accountName({
receiver: "0123456789",
channel: 2,
currency: "NGN",
bankCode: "058",
});
const payout = await moolre.transfers.initiate({
railId: "ng_bank_transfer",
receiver: "0123456789",
bankCode: "058",
amount: 5000,
currency: "NGN",
});Ghana bank payouts use railId: "gh_bank_transfer" with the bank code from moolre.banks.list({ country: "ghana" }).
Mobile money collection (MoMo)
Supported: Ghana (GHS) and Nigeria (NGN) mobile money.
The SDK auto-generates:
externalRef(Morapay-XXXXXX) when omittedcurrencyfrom phone country (GHS/NGN)channelfrom phone + optionalproviderhint
OTP flow (recommended for production)
Most Ghana/Nigeria MoMo collections require OTP:
Step 1: initiate() → TP14 (otp_required) — Moolre sends OTP SMS to payer
Step 2: submitOtp() → TP17 (otp_verified) → TR099 (payment_requested)// Step 1 — start collection
const init = await moolre.payments.initiate({
payerPhone: "0541234567", // or +233541234567
amount: 50, // major units (50.00 GHS)
referenceMessage: "Order #1234",
});
if (init.otpRequired) {
// Show OTP input in your UI — payer receives SMS from Moolre
console.log("Reference:", init.externalRef);
const result = await moolre.payments.submitOtp({
payerPhone: "0541234567",
amount: 50,
externalRef: init.externalRef, // must match step 1
otp: "123456",
});
console.log(result.completed, result.code); // true, TR099
}Non-OTP / one-shot flow
If you already have the OTP (or Moolre returns TR099 directly):
const result = await moolre.payments.collect({
payerPhone: "0541234567",
amount: 50,
externalRef: "Morapay-ABC123",
otp: "123456", // when provided, runs full OTP → payment flow
});
// Without OTP — initiate only
const pending = await moolre.payments.collect({
payerPhone: "08031234567",
amount: 1000,
currency: "NGN",
provider: "MTN", // required for Nigeria if channel can't be inferred
});Nigeria provider hints
| Provider | Pass as provider |
|-------------|--------------------|
| MTN | "MTN" |
| AirtelTigo | "AirtelTigo" |
| Telecel | "Telecel" |
Hosted payment links
For card/bank checkout pages hosted by Moolre:
const link = await moolre.links.create({
amount: 1000,
currency: "NGN",
email: "[email protected]",
callbackUrl: "https://yourapp.com/webhooks/moolre/payment",
redirectUrl: "https://yourapp.com/payment/success",
});
console.log(link.authorizationUrl); // send customer here
console.log(link.reference); // Morapay-XXXXXXCheck payment status:
const status = await moolre.transactions.getStatus({
reference: link.reference,
});
import { mapTxStatusLabel } from "@morapay/moolre";
console.log(mapTxStatusLabel(status.txstatus)); // pending | success | failedOutbound transfers (payouts)
Send money to Ghana MoMo or Ghana/Nigeria bank accounts.
Important: Transfer channels differ from collection channels. Ghana MTN collection uses channel
13; transfer uses channel1.
// Ghana mobile money
const payout = await moolre.transfers.initiate({
railId: "gh_mobile_money",
receiver: "0537161293",
amount: 5,
currency: "GHS",
provider: "MTN", // optional — defaults to MTN (channel 1)
});
// Nigeria bank
const bankPayout = await moolre.transfers.initiate({
railId: "ng_bank_transfer",
receiver: "0123456789", // account number
bankCode: "058", // bank code from moolre.banks.list()
amount: 5000,
currency: "NGN",
});
console.log(payout.accepted, payout.phase, payout.moolreId);Available rails: gh_mobile_money, ng_bank_transfer, gh_bank_transfer.
List banks:
const banks = await moolre.banks.list({ country: "nigeria" });Validate account name before payout:
const name = await moolre.validate.accountName({
receiver: "0123456789",
channel: 2,
currency: "NGN",
bankCode: "058",
});SMS
SMS uses the live Moolre SMS API regardless of mode (sandbox/live for payments).
await moolre.sms.send({
to: "+233541234567",
message: "Your order has shipped.",
});
// Payment OTP helper (your own OTP, not Moolre's MoMo OTP)
await moolre.sms.sendPaymentOtp({
to: "0541234567",
otp: "482910",
externalRef: "Morapay-ABC123",
brandName: "MyStore",
});Virtual bank accounts
Create dedicated bank account numbers for inbound bank transfers:
const account = await moolre.virtualAccounts.create({
currency: "NGN",
email: "[email protected]",
firstName: "Jane",
lastName: "Doe",
phone: "08031234567",
amount: 5000, // optional expected amount
});
console.log(account.accountno, account.bankname);Error handling
All API failures throw MoolreError:
import { MoolreError, MoolreValidationError } from "@morapay/moolre";
try {
await moolre.payments.initiate({ payerPhone: "invalid", amount: 10 });
} catch (error) {
if (error instanceof MoolreValidationError) {
// bad input before API call
}
if (error instanceof MoolreError) {
console.log(error.moolreCode, error.statusCode, error.message);
}
}Validation errors (invalid phone, missing bank code, etc.) throw MoolreValidationError before any network request.
API reference
| Resource | Methods | Description |
|----------|---------|-------------|
| moolre.payments | initiate, submitOtp, collect | MoMo collection |
| moolre.links | create | Hosted payment links |
| moolre.transactions | getStatus | Transaction status lookup |
| moolre.transfers | initiate | Outbound payouts |
| moolre.banks | list | Bank directory |
| moolre.validate | accountName | Account name lookup |
| moolre.virtualAccounts | create | Virtual bank accounts |
| moolre.sms | send, sendPaymentOtp | SMS via Moolre VAS |
Helpers
import {
generateExternalRef,
normalizePhone,
classifyPaymentInitCode,
MOOLRE_PAYMENT_INIT_OTP_REQUIRED,
MOOLRE_TX_STATUS,
} from "@morapay/moolre";Environment variable reference
| Variable | Required | Description |
|----------|----------|-------------|
| MOOLRE_USERNAME | Yes | X-API-USER header |
| MOOLRE_ACCOUNT_NUMBER | Yes | Wallet account number |
| MOOLRE_PRIVATE_API_KEY | Live transact | Private key for payment/transfer/validate |
| MOOLRE_PUBLIC_API_KEY | Live links/status | Public key for links, status, virtual bank |
| MOOLRE_MODE or MOOLRE_ENV | No | live (default, production) or sandbox / test (integration testing) |
| MOOLRE_SANDBOX_ACCOUNT_NUMBER | Sandbox | Override account in sandbox |
| MOOLRE_SMS_API_KEY | SMS | VAS key (X-API-VASKEY) |
| MOOLRE_SMS_SENDER_ID | SMS | Registered sender id |
| MOOLRE_PAYMENT_CALLBACK_URL | No | Default callback for payment links |
| MOOLRE_TRANSFER_CALLBACK_URL | No | Default callback for transfers |
| MOOLRE_API_BASE_URL | No | Override live API base |
| MOOLRE_SANDBOX_API_BASE_URL | No | Override sandbox API base |
| MOOLRE_TIMEOUT_MS | No | Request timeout (default 15000) |
Common pitfalls (and how this SDK fixes them)
- Wrong channel id — Collection MTN is
13, transfer MTN is1. The SDK picks the right channel per operation. - Missing OTP follow-up — After
TP17, you must call payment again forTR099.submitOtp()andcollect({ otp })do this automatically. - Phone format — Pass
054…,+233…,080…, or+234…. The SDK normalizes to Moolre's expected local format. - Amount format — Pass
50or"50.00". The SDK formats to two decimal places. - Sandbox vs live keys — Sandbox only needs username + account number. Live requires private/public keys.
- External references — Must be unique per transaction. Auto-generated when omitted.
License
MIT © Morapay
