mozosubz-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # Mozosubz SDK
2
+
3
+ Buy cheap data and airtime in Nigeria programmatically via the [Mozosubz V1 API](https://mozosubz.xyz/doc).
4
+
5
+ Purchases process synchronously in milliseconds — the API response you get back **is** the final transaction status (`success`, `transaction_id`, amount, and your new wallet balance). No polling or webhooks needed.
6
+
7
+ | Method | Endpoint |
8
+ |---|---|
9
+ | `getDataPlans(service)` | `GET /api/v1/data/plans?service=...` |
10
+ | `buyData({ service, planId, phone })` | `POST /api/v1/data/purchase` |
11
+ | `buyAirtime({ network, amount, phone })` | `POST /api/v1/airtime/purchase` |
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install mozosubz-sdk
17
+ ```
18
+
19
+ Zero dependencies. Node 18+.
20
+
21
+ ## Quick start
22
+
23
+ ```js
24
+ const { Mozosubz } = require('mozosubz-sdk');
25
+
26
+ const client = new Mozosubz({ apiKey: process.env.MOZOSUBZ_API_KEY });
27
+
28
+ // 1. Fetch plans in real-time (docs: never hardcode plan IDs)
29
+ const { plans } = await client.getDataPlans('mtn_sme');
30
+
31
+ // 2. Buy data — response is the final status
32
+ const order = await client.buyData({
33
+ service: 'mtn_sme',
34
+ planId: plans[0].id,
35
+ phone: '08012345678',
36
+ });
37
+ console.log(order.transaction_id, order.balance_after);
38
+
39
+ // 3. Buy airtime (minimum ₦50)
40
+ await client.buyAirtime({ network: 'mtn', amount: 100, phone: '08012345678' });
41
+ ```
42
+
43
+ Get your Connect Key (format `connect_...`) from the API Keys page on [mozosubz.xyz](https://mozosubz.xyz) — the SDK sends it in the `X-Connect-Key` header for you.
44
+
45
+ ## Data services
46
+
47
+ `mtn_sme`, `mtn_datashare`, `mtn_gifting`, `mtn_awoof`, `glo_data`, `glo_sme`, `airtel_sme`, `airtel_gifting`, `etisalat_data`
48
+
49
+ Also available as constants: `Mozosubz.DATA_SERVICES.MTN_SME`, etc.
50
+
51
+ ## Airtime networks
52
+
53
+ `mtn`, `glo`, `airtel`, `etisalat` (9mobile)
54
+
55
+ ## Error handling
56
+
57
+ All failures throw `MozosubzError` with helpers mapped to the documented status codes:
58
+
59
+ ```js
60
+ const { MozosubzError } = require('mozosubz-sdk');
61
+
62
+ try {
63
+ await client.buyData({ service: 'mtn_sme', planId: 'plan_1', phone: '08012345678' });
64
+ } catch (err) {
65
+ if (err instanceof MozosubzError) {
66
+ if (err.isAuthError) { // 401 — bad/missing key
67
+ } else if (err.isInsufficientBalance) { // 402 — fund your wallet
68
+ } else if (err.isRateLimited) { // 429 — 10,000 requests/day limit
69
+ } else {
70
+ console.error(err.status, err.message); // 400, 500, or network failure (status null)
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Safety choices
77
+
78
+ - **Purchases are never auto-retried.** A network failure mid-purchase is ambiguous — retrying could double-charge your wallet. Only the read-only `getDataPlans` retries (2× by default).
79
+ - Phone numbers are validated client-side (`08012345678` form) before any request is sent.
80
+ - Airtime below the documented ₦50 minimum is rejected locally.
81
+
82
+ ## Notes
83
+
84
+ - Rate limit: 10,000 requests/day.
85
+ - Keep your key server-side only — never ship it in client-side code.
86
+ - Base URL: `https://mozosubz.xyz/api/v1` (overridable via `new Mozosubz({ baseUrl })`).
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "mozosubz-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Node.js SDK for the Mozosubz V1 API — buy cheap data and airtime in Nigeria programmatically. Purchases process in milliseconds.",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "type": "commonjs",
8
+ "files": [
9
+ "src",
10
+ "README.md"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "homepage": "https://mozosubz.xyz/doc",
16
+ "keywords": [
17
+ "mozosubz",
18
+ "vtu",
19
+ "data",
20
+ "airtime",
21
+ "nigeria",
22
+ "mtn",
23
+ "glo",
24
+ "airtel",
25
+ "9mobile"
26
+ ],
27
+ "license": "MIT"
28
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ export interface DataPlan {
2
+ id: string;
3
+ displayName: string;
4
+ price: number;
5
+ validity: string;
6
+ }
7
+
8
+ export interface DataPlansResponse {
9
+ success: true;
10
+ service: string;
11
+ plans: DataPlan[];
12
+ }
13
+
14
+ export interface DataPurchaseResponse {
15
+ success: true;
16
+ transaction_id: string;
17
+ plan: string;
18
+ phone: string;
19
+ amount: number;
20
+ balance_after: number;
21
+ }
22
+
23
+ export interface AirtimePurchaseResponse {
24
+ success: true;
25
+ transaction_id: string;
26
+ network: string;
27
+ phone: string;
28
+ amount: number;
29
+ balance_after: number;
30
+ }
31
+
32
+ export type DataService =
33
+ | 'mtn_sme'
34
+ | 'mtn_datashare'
35
+ | 'mtn_gifting'
36
+ | 'mtn_awoof'
37
+ | 'glo_data'
38
+ | 'glo_sme'
39
+ | 'airtel_sme'
40
+ | 'airtel_gifting'
41
+ | 'etisalat_data';
42
+
43
+ export type AirtimeNetwork = 'mtn' | 'glo' | 'airtel' | 'etisalat';
44
+
45
+ export declare class MozosubzError extends Error {
46
+ status: number | null;
47
+ body: object | null;
48
+ isAuthError: boolean;
49
+ isInsufficientBalance: boolean;
50
+ isRateLimited: boolean;
51
+ }
52
+
53
+ export interface MozosubzOptions {
54
+ apiKey: string;
55
+ baseUrl?: string;
56
+ timeoutMs?: number;
57
+ maxRetries?: number;
58
+ }
59
+
60
+ export declare class Mozosubz {
61
+ constructor(options: MozosubzOptions);
62
+ getDataPlans(service: DataService): Promise<DataPlansResponse>;
63
+ buyData(params: { service: DataService; planId: string; phone: string }): Promise<DataPurchaseResponse>;
64
+ buyAirtime(params: { network: AirtimeNetwork; amount: number; phone: string }): Promise<AirtimePurchaseResponse>;
65
+
66
+ static DATA_SERVICES: Record<string, DataService>;
67
+ static AIRTIME_NETWORKS: Record<string, AirtimeNetwork>;
68
+ static MIN_AIRTIME_AMOUNT: number;
69
+ }
70
+
71
+ export declare const DATA_SERVICES: Record<string, DataService>;
72
+ export declare const AIRTIME_NETWORKS: Record<string, AirtimeNetwork>;
73
+ export declare const MIN_AIRTIME_AMOUNT: number;
package/src/index.js ADDED
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Mozosubz Node.js SDK
5
+ *
6
+ * Built strictly on the documented Mozosubz V1 API (https://mozosubz.xyz/doc):
7
+ * - GET /api/v1/data/plans?service=<service_id>
8
+ * - POST /api/v1/data/purchase
9
+ * - POST /api/v1/airtime/purchase
10
+ *
11
+ * Auth: X-Connect-Key header. All responses are JSON.
12
+ */
13
+
14
+ const BASE_URL = 'https://mozosubz.xyz/api/v1';
15
+
16
+ /** Data service IDs, exactly as listed in the API reference. */
17
+ const DATA_SERVICES = Object.freeze({
18
+ MTN_SME: 'mtn_sme',
19
+ MTN_DATASHARE: 'mtn_datashare',
20
+ MTN_GIFTING: 'mtn_gifting',
21
+ MTN_AWOOF: 'mtn_awoof',
22
+ GLO_DATA: 'glo_data',
23
+ GLO_SME: 'glo_sme',
24
+ AIRTEL_SME: 'airtel_sme',
25
+ AIRTEL_GIFTING: 'airtel_gifting',
26
+ ETISALAT_DATA: 'etisalat_data',
27
+ });
28
+
29
+ /** Airtime network IDs, exactly as listed in the API reference. */
30
+ const AIRTIME_NETWORKS = Object.freeze({
31
+ MTN: 'mtn',
32
+ GLO: 'glo',
33
+ AIRTEL: 'airtel',
34
+ ETISALAT: 'etisalat',
35
+ });
36
+
37
+ /** Documented minimum airtime amount (₦). */
38
+ const MIN_AIRTIME_AMOUNT = 50;
39
+
40
+ class MozosubzError extends Error {
41
+ /**
42
+ * @param {string} message
43
+ * @param {number|null} status HTTP status code, null for network failures
44
+ * @param {object|null} body Parsed response body if available
45
+ */
46
+ constructor(message, status = null, body = null) {
47
+ super(message);
48
+ this.name = 'MozosubzError';
49
+ this.status = status;
50
+ this.body = body;
51
+ // Documented statuses: 400, 401, 402, 429, 500
52
+ this.isAuthError = status === 401;
53
+ this.isInsufficientBalance = status === 402;
54
+ this.isRateLimited = status === 429;
55
+ }
56
+ }
57
+
58
+ class Mozosubz {
59
+ /**
60
+ * @param {object} options
61
+ * @param {string} options.apiKey Your Connect Key (format: connect_...)
62
+ * @param {string} [options.baseUrl] Override base URL (default: https://mozosubz.xyz/api/v1)
63
+ * @param {number} [options.timeoutMs] Request timeout in ms (default: 30000)
64
+ * @param {number} [options.maxRetries] Retries for network failures on GET requests only (default: 2)
65
+ */
66
+ constructor({ apiKey, baseUrl = BASE_URL, timeoutMs = 30000, maxRetries = 2 } = {}) {
67
+ if (!apiKey || typeof apiKey !== 'string') {
68
+ throw new MozosubzError('apiKey is required. Get one from the API Keys page on mozosubz.xyz.');
69
+ }
70
+ this.apiKey = apiKey;
71
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
72
+ this.timeoutMs = timeoutMs;
73
+ this.maxRetries = maxRetries;
74
+ }
75
+
76
+ /**
77
+ * GET /data/plans — list plans for a data service.
78
+ * Docs: fetch plans in real-time, don't hardcode plan IDs.
79
+ * @param {string} service e.g. 'mtn_sme', 'glo_data' (see Mozosubz.DATA_SERVICES)
80
+ * @returns {Promise<{success: boolean, service: string, plans: Array<{id: string, displayName: string, price: number, validity: string}>}>}
81
+ */
82
+ async getDataPlans(service) {
83
+ assertOneOf('service', service, Object.values(DATA_SERVICES));
84
+ const qs = new URLSearchParams({ service }).toString();
85
+ // Safe to retry: read-only request.
86
+ return this._request('GET', `/data/plans?${qs}`, null, { retriable: true });
87
+ }
88
+
89
+ /**
90
+ * POST /data/purchase — buy a data plan.
91
+ * NOT retried automatically: retrying a purchase after an ambiguous network
92
+ * failure could double-charge your wallet.
93
+ * @param {object} params
94
+ * @param {string} params.service Data service ID, e.g. 'mtn_sme'
95
+ * @param {string} params.planId Plan ID from getDataPlans()
96
+ * @param {string} params.phone Nigerian phone number, e.g. '08012345678'
97
+ * @returns {Promise<{success: boolean, transaction_id: string, plan: string, phone: string, amount: number, balance_after: number}>}
98
+ */
99
+ async buyData({ service, planId, phone } = {}) {
100
+ assertOneOf('service', service, Object.values(DATA_SERVICES));
101
+ assertNonEmptyString('planId', planId);
102
+ assertPhone(phone);
103
+ return this._request('POST', '/data/purchase', {
104
+ service,
105
+ plan_id: planId,
106
+ phone,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * POST /airtime/purchase — buy airtime.
112
+ * NOT retried automatically (same double-charge risk as buyData).
113
+ * @param {object} params
114
+ * @param {string} params.network 'mtn' | 'glo' | 'airtel' | 'etisalat'
115
+ * @param {number} params.amount Amount in ₦. Minimum ₦50 per the docs.
116
+ * @param {string} params.phone Nigerian phone number
117
+ * @returns {Promise<{success: boolean, transaction_id: string, network: string, phone: string, amount: number, balance_after: number}>}
118
+ */
119
+ async buyAirtime({ network, amount, phone } = {}) {
120
+ assertOneOf('network', network, Object.values(AIRTIME_NETWORKS));
121
+ if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < MIN_AIRTIME_AMOUNT) {
122
+ throw new MozosubzError(`amount must be a number >= ₦${MIN_AIRTIME_AMOUNT}`);
123
+ }
124
+ assertPhone(phone);
125
+ return this._request('POST', '/airtime/purchase', { network, amount, phone });
126
+ }
127
+
128
+ async _request(method, path, body, { retriable = false } = {}) {
129
+ const url = `${this.baseUrl}${path}`;
130
+ const attempts = retriable ? this.maxRetries + 1 : 1;
131
+ let lastNetworkError;
132
+
133
+ for (let attempt = 1; attempt <= attempts; attempt++) {
134
+ const controller = new AbortController();
135
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
136
+ let res;
137
+ try {
138
+ res = await fetch(url, {
139
+ method,
140
+ headers: {
141
+ 'X-Connect-Key': this.apiKey,
142
+ Accept: 'application/json',
143
+ ...(body ? { 'Content-Type': 'application/json' } : {}),
144
+ },
145
+ body: body ? JSON.stringify(body) : undefined,
146
+ signal: controller.signal,
147
+ });
148
+ } catch (err) {
149
+ clearTimeout(timer);
150
+ lastNetworkError = err;
151
+ if (attempt < attempts) {
152
+ await sleep(300 * attempt);
153
+ continue;
154
+ }
155
+ throw new MozosubzError(`Network error calling ${method} ${path}: ${err.message}`, null, null);
156
+ }
157
+ clearTimeout(timer);
158
+
159
+ let json = null;
160
+ try {
161
+ json = await res.json();
162
+ } catch {
163
+ // Non-JSON response despite docs saying all responses are JSON.
164
+ }
165
+
166
+ if (res.ok && json && json.success === true) return json;
167
+
168
+ // Error shape confirmed against the live API: {"success":false,"error":"..."}
169
+ const message =
170
+ (json && (json.error || json.message)) || `HTTP ${res.status} from ${method} ${path}`;
171
+ throw new MozosubzError(message, res.status, json);
172
+ }
173
+
174
+ throw new MozosubzError(`Network error: ${lastNetworkError?.message}`, null, null);
175
+ }
176
+ }
177
+
178
+ function assertNonEmptyString(name, value) {
179
+ if (!value || typeof value !== 'string') {
180
+ throw new MozosubzError(`${name} is required and must be a non-empty string`);
181
+ }
182
+ }
183
+
184
+ function assertOneOf(name, value, allowed) {
185
+ assertNonEmptyString(name, value);
186
+ if (!allowed.includes(value)) {
187
+ throw new MozosubzError(`${name} must be one of: ${allowed.join(', ')} (got "${value}")`);
188
+ }
189
+ }
190
+
191
+ // Docs say "Nigerian phone number" and show the 0-prefixed 11-digit form
192
+ // (e.g. 08012345678). Validate that documented form only.
193
+ function assertPhone(phone) {
194
+ assertNonEmptyString('phone', phone);
195
+ if (!/^0\d{10}$/.test(phone)) {
196
+ throw new MozosubzError('phone must be an 11-digit Nigerian number starting with 0, e.g. 08012345678');
197
+ }
198
+ }
199
+
200
+ function sleep(ms) {
201
+ return new Promise((r) => setTimeout(r, ms));
202
+ }
203
+
204
+ Mozosubz.DATA_SERVICES = DATA_SERVICES;
205
+ Mozosubz.AIRTIME_NETWORKS = AIRTIME_NETWORKS;
206
+ Mozosubz.MIN_AIRTIME_AMOUNT = MIN_AIRTIME_AMOUNT;
207
+
208
+ module.exports = { Mozosubz, MozosubzError, DATA_SERVICES, AIRTIME_NETWORKS, MIN_AIRTIME_AMOUNT };