@sonatel-os/juf
The community SDK for Orange Money, SMS, Email & Sonatel APIs.
Built on the Orange Developer Platform — open to all developers.
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/juf
3. Configure
cp .env.example .env
JUF_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: 'user@example.com',
from: 'hello@myapp.com',
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,
});
Full response shape
| 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, contract
Bundled 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 build
Commits must follow Conventional Commits (enforced by commitlint).
MIT License — See LICENCE
Made with care by Mohamed Johnson at Sonatel
Built on the Orange Developer Platform