@sonatel-os/juf
v0.6.0
Published
The community SDK for Orange Money, SMS, Email & Sonatel APIs on the Orange Developer Platform.
Maintainers
Readme
Why JUF?
The Orange Developer Platform exposes powerful APIs for payments, messaging, and more — but provides no official SDK. JUF fills that gap.
| What you get | Without JUF | With JUF |
|---|---|---|
| OAuth2 | Manual token fetch, caching, refresh | Automatic — one call, cached 240s |
| Payments | Raw HTTP, manual payload construction | payment.preparePaymentCheckout() |
| QR Codes | Build payloads, format amounts, manage headers | payment.createPaymentQRCode() |
| SMS / Email | Auth + HTTP + error parsing | communication.sendSMS() |
| Error handling | Parse each endpoint's error shape | Consistent JufError hierarchy |
| Validation | Hope for the best | Superstruct schemas catch bad input before it hits the API |
Getting Started
1. Get your API credentials
Sign up at developer.orange-sonatel.com (free), create an application, and grab your client_id and client_secret. Sandbox access is immediate — no approval needed.
2. Install
yarn add @sonatel-os/juf
# or
npm install @sonatel-os/juf3. Configure
cp .env.example .envJUF_APIGEE_CLIENT_ID="<your-client-id>"
JUF_APIGEE_CLIENT_SECRET="<your-client-secret>"See .env.example for all options (production, preprod, APM, logging).
4. Use
import { authentication, communication, payment } from '@sonatel-os/juf';
// Authenticate (tokens are cached automatically)
const { access_token } = await authentication.debug();
// Accept a payment via QR code
const { qrCode, deepLinks } = await payment.createPaymentQRCode({
merchant: { code: 123456, sitename: 'CoolShop' },
bill: { amount: 2500, reference: 'ORDER-42' },
});
// Send a confirmation SMS
await communication.sendSMS({
body: 'Payment received! Thank you.',
to: '+221770000000',
senderName: 'CoolShop',
});Subpath Imports (recommended)
Import only what you need for smaller bundles and clearer dependency graphs:
// Instead of importing everything:
import { authentication, payment } from '@sonatel-os/juf';
// Import only the domain you need:
import { Authentication } from '@sonatel-os/juf/auth';
import { Payment } from '@sonatel-os/juf/payment';
import { Communication } from '@sonatel-os/juf/communication';
import { ValidationError, AuthenticationError } from '@sonatel-os/juf/core';
// With DI, you control initialization:
const auth = new Authentication({ config, cache, client, logger });
const pay = new Payment({ authService: auth, client, config, logger });| Subpath | Exports |
|---|---|
| @sonatel-os/juf/auth | Authentication class |
| @sonatel-os/juf/communication | Communication class, EmailStructure, SmsStructure |
| @sonatel-os/juf/payment | Payment, QRCodeDecoder classes, CheckoutPaymentStructure, QRCodePaymentStructure, QRCodeDecodePaymentStructure |
| @sonatel-os/juf/core | Errors, validation, logger, cache, constants, requester |
The root import (
@sonatel-os/juf) still works and will continue to work until v2.0.0.
API Reference
Authentication
import { Authentication } from '@sonatel-os/juf/auth';
const auth = Authentication.init();auth.debug()
Fetches a fresh OAuth2 token or returns the cached one (TTL: 240s).
const { access_token, token_type, expires_in } = await auth.debug();Communication
import { Communication } from '@sonatel-os/juf/communication';
const comm = Communication.init();comm.sendEmail({ subject, to, from, body, html? })
const { id, status } = await comm.sendEmail({
subject: 'Welcome!',
to: '[email protected]',
from: '[email protected]',
body: '<h1>Welcome aboard!</h1>',
html: true,
});| Param | Type | Required | Description |
|---|---|:---:|---|
| subject | string | Yes | Email subject line |
| to | string | Yes | Recipient address |
| from | string | Yes | Sender address |
| body | string | Yes | Email content |
| html | boolean | — | true if body is HTML |
comm.sendSMS({ body, to, senderName, confidential?, scheduledFor? })
const { id, status } = await comm.sendSMS({
body: 'Your OTP is 4829',
to: '+221770000000',
senderName: 'MyApp',
});| Param | Type | Required | Default | Description |
|---|---|:---:|:---:|---|
| body | string | Yes | — | Message content |
| to | string | Yes | — | Phone number |
| senderName | string | Yes | — | Sender display name |
| confidential | boolean | — | true | Mark as confidential |
| scheduledFor | string | — | — | ISO 8601 datetime |
Payment
import { Payment } from '@sonatel-os/juf/payment';
const pay = Payment.init();pay.preparePaymentCheckout({ merchant, bill, urls })
Creates a payment session and returns a checkout link.
const { link, secret } = await pay.preparePaymentCheckout({
merchant: { code: 123456, sitename: 'your-sitename' },
bill: { amount: 1000, reference: 'INV-2024-001' },
urls: {
success: 'https://my.site/success',
failed: 'https://my.site/failed',
cancel: 'https://my.site/canceled',
callback: 'https://my.site/webhook',
},
});
// Redirect your user to `link`pay.createPaymentQRCode({ merchant, bill, urls?, metadata?, validity? })
Generates a QR code for mobile payment apps (Orange Money, MaxIt).
const { qrId, qrCode, deepLinks, shortLink } = await pay.createPaymentQRCode({
merchant: { code: 123456, sitename: 'your-sitename' },
bill: { amount: 500, reference: 'TIP-007' },
metadata: { table: 12, waiter: 'Amadou' },
validity: 300,
});| Field | Type | Description |
|---|---|---|
| deepLink | string | Universal deep link |
| deepLinks.MAXIT | string | MaxIt-specific link |
| deepLinks.OM | string | Orange Money link |
| qrCode | string | Base64 QR code image |
| validity | number | Seconds remaining |
| metadata | object | Your custom metadata |
| shortLink | string | Shortened payment URL |
| qrId | string | QR code identifier |
pay.decodeQrCode({ id })
Reads back the contents of a generated QR code. This is a privileged operation — only applications with explicit decode_qr_sp_authorization credentials can use it. It uses static SP authorization instead of the OAuth2 Bearer token.
const { content } = await pay.decodeQrCode({ id: 'doyaT9sH3rGFph_ZuKIs' });
console.log(content.amount, content.reference);You can also use the standalone QRCodeDecoder class directly:
import { QRCodeDecoder } from '@sonatel-os/juf/payment';
const decoder = QRCodeDecoder.init();
const { content } = await decoder.decode({ id: 'doyaT9sH3rGFph_ZuKIs' });Error Handling
Every error thrown by JUF follows one consistent shape:
import { ValidationError, AuthenticationError, ExternalServiceError } from '@sonatel-os/juf/core';
try {
await pay.preparePaymentCheckout({ /* bad data */ });
} catch (error) {
console.log(error.toJSON());
// {
// success: false,
// error: {
// code: 'JUF_VALIDATION_ERROR',
// message: 'Validation failed for preparePaymentCheckout: ...',
// details: [...]
// }
// }
}| Error Class | Code | Status | When |
|---|---|:---:|---|
| ValidationError | JUF_VALIDATION_ERROR | 400 | Bad input (wrong types, missing fields, invalid URLs) |
| AuthenticationError | JUF_AUTH_ERROR | 401 | OAuth2 failure (bad credentials, expired) |
| ExternalServiceError | JUF_EXTERNAL_SERVICE_ERROR | varies | Upstream API error |
All errors extend JufError which extends native Error — instanceof checks work as expected.
Project Structure
src/
auth/ # OAuth2 client credentials flow
communication/ # Email & SMS via Apigee
payment/ # Checkout, QR codes, decode
core/ # Errors, validation, logger, cache, HTTP client
config/ # Environment-based configuration loader
tests/ # 174 tests — unit, service, contractBundled Logger
JUF ships with @sonatel-os/juf-xpress-logger included — no extra install needed. It's a structured Express-aware logger maintained in its own SDK repository.
import { logger } from '@sonatel-os/juf';
logger.bootstrap({
appName: 'my-service',
logConsole: true,
});JUF also has its own internal lightweight logger (used for library diagnostics) separate from
juf-xpress-logger. SetJUF_LOG_LEVEL=debugto see internal debug output.
Notes
- Tokens are cached for 240 seconds — no redundant auth calls
- All redirect URLs are validated (HTTP/HTTPS only) to prevent open redirect attacks
- Validation errors are thrown, not silently swallowed — always use try/catch
- If the QR code service is down during checkout, the flow gracefully falls back to USSD
- The internal logger sanitizes sensitive fields (tokens, secrets, passwords) before logging
- Services support dependency injection for full testability
Contributing
git clone <repo-url> && cd juf-js
yarn install
yarn test # Run 174 tests
yarn lint # ESLint check
yarn build # Dual ESM/CJS buildCommits must follow Conventional Commits (enforced by commitlint).
