semaphoreco 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,435 @@
1
+ # semaphoreco
2
+
3
+ [![npm version](https://img.shields.io/npm/v/semaphoreco.svg)](https://www.npmjs.com/package/semaphoreco)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Official TypeScript/JavaScript client for the [Semaphore.co](https://semaphore.co) SMS Gateway API (Philippines).
7
+
8
+ > **Note:** This package was previously published as `semaphore-client-js`. If you're migrating, simply update your package name - the API is unchanged.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install semaphoreco
14
+ ```
15
+
16
+ Or with yarn:
17
+
18
+ ```bash
19
+ yarn add semaphoreco
20
+ ```
21
+
22
+ Or with pnpm:
23
+
24
+ ```bash
25
+ pnpm add semaphoreco
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```typescript
31
+ import { SemaphoreClient } from 'semaphoreco';
32
+
33
+ const client = new SemaphoreClient({ apiKey: 'your-api-key' });
34
+
35
+ // Send an SMS
36
+ const result = await client.messages.send({
37
+ number: '639171234567',
38
+ message: 'Hello from Semaphore!',
39
+ sendername: 'SEMAPHORE',
40
+ });
41
+
42
+ console.log('Message ID:', result.message_id);
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Create a client instance with your API key:
48
+
49
+ ```typescript
50
+ import { SemaphoreClient } from 'semaphoreco';
51
+
52
+ const client = new SemaphoreClient({
53
+ apiKey: 'your-api-key', // Required (or set SEMAPHORE_API_KEY env var)
54
+ baseUrl: 'https://api.semaphore.co/api/v4', // Optional, this is the default
55
+ timeout: 30000, // Optional, request timeout in ms (default: 30000)
56
+ onRetry: (attempt, delayMs) => {
57
+ console.log(`Retry attempt ${attempt} after ${delayMs}ms`);
58
+ },
59
+ });
60
+ ```
61
+
62
+ ### Configuration Options
63
+
64
+ | Option | Type | Default | Description |
65
+ |--------|------|---------|-------------|
66
+ | `apiKey` | `string` | `SEMAPHORE_API_KEY` env var | Your Semaphore API key |
67
+ | `baseUrl` | `string` | `https://api.semaphore.co/api/v4` | API base URL |
68
+ | `timeout` | `number` | `30000` | Request timeout in milliseconds |
69
+ | `onRetry` | `function` | `undefined` | Callback when rate-limited request is retried |
70
+
71
+ ### Environment Variable
72
+
73
+ You can set your API key via environment variable instead of passing it to the constructor:
74
+
75
+ ```bash
76
+ export SEMAPHORE_API_KEY=your-api-key
77
+ ```
78
+
79
+ ```typescript
80
+ // API key will be read from SEMAPHORE_API_KEY
81
+ const client = new SemaphoreClient();
82
+ ```
83
+
84
+ ## API Reference
85
+
86
+ ### Messages (`client.messages`)
87
+
88
+ #### `send(params)`
89
+
90
+ Send an SMS to one or more recipients.
91
+
92
+ ```typescript
93
+ // Send to a single recipient
94
+ const response = await client.messages.send({
95
+ number: '639171234567',
96
+ message: 'Hello!',
97
+ sendername: 'SEMAPHORE',
98
+ });
99
+
100
+ console.log(response.message_id); // 12345
101
+ console.log(response.status); // 'Queued'
102
+ ```
103
+
104
+ **Bulk send** - send to multiple recipients (up to 1000):
105
+
106
+ ```typescript
107
+ const responses = await client.messages.send({
108
+ number: ['639171234567', '639181234567', '639191234567'],
109
+ message: 'Hello everyone!',
110
+ sendername: 'SEMAPHORE',
111
+ });
112
+
113
+ // Returns array of responses
114
+ for (const response of responses) {
115
+ console.log(`Sent to ${response.recipient}: ${response.message_id}`);
116
+ }
117
+ ```
118
+
119
+ **Request Parameters:**
120
+
121
+ | Parameter | Type | Required | Description |
122
+ |-----------|------|----------|-------------|
123
+ | `number` | `string \| string[]` | Yes | Recipient number(s). Max 1000 for bulk |
124
+ | `message` | `string` | Yes | Message content |
125
+ | `sendername` | `string` | Yes | Approved sender name |
126
+
127
+ **Response:**
128
+
129
+ ```typescript
130
+ interface SendMessageResponse {
131
+ message_id: number;
132
+ user_id: number;
133
+ user: string;
134
+ account_id: number;
135
+ account: string;
136
+ recipient: string;
137
+ message: string;
138
+ sender_name: string;
139
+ network: string;
140
+ status: string;
141
+ type: string;
142
+ source: string;
143
+ created_at: string;
144
+ updated_at: string;
145
+ }
146
+ ```
147
+
148
+ #### `list(params?)`
149
+
150
+ List sent messages with optional filtering.
151
+
152
+ ```typescript
153
+ // List all messages
154
+ const messages = await client.messages.list();
155
+
156
+ // List with filters
157
+ const filtered = await client.messages.list({
158
+ page: 1,
159
+ limit: 50,
160
+ status: 'Sent',
161
+ startDate: '2024-01-01',
162
+ endDate: '2024-01-31',
163
+ });
164
+ ```
165
+
166
+ **Filter Parameters:**
167
+
168
+ | Parameter | Type | Description |
169
+ |-----------|------|-------------|
170
+ | `page` | `number` | Page number for pagination |
171
+ | `limit` | `number` | Number of results per page |
172
+ | `startDate` | `string` | Filter by start date (YYYY-MM-DD) |
173
+ | `endDate` | `string` | Filter by end date (YYYY-MM-DD) |
174
+ | `network` | `string` | Filter by network |
175
+ | `status` | `string` | Filter by status |
176
+ | `sendername` | `string` | Filter by sender name |
177
+
178
+ #### `get(id)`
179
+
180
+ Retrieve a specific message by ID.
181
+
182
+ ```typescript
183
+ const message = await client.messages.get(12345);
184
+
185
+ console.log(message.status); // 'Sent'
186
+ console.log(message.recipient); // '639171234567'
187
+ console.log(message.message); // 'Hello!'
188
+ ```
189
+
190
+ ---
191
+
192
+ ### OTP (`client.otp`)
193
+
194
+ Send OTP messages via priority route with no rate limit. Each OTP message costs 2 credits per 160 characters.
195
+
196
+ #### `send(params)`
197
+
198
+ Send an OTP message. The API generates the OTP code and replaces the `{otp}` placeholder in your message.
199
+
200
+ ```typescript
201
+ const response = await client.otp.send({
202
+ number: '639171234567',
203
+ message: 'Your verification code is {otp}',
204
+ sendername: 'MyApp',
205
+ });
206
+
207
+ console.log('OTP code:', response.code); // '123456'
208
+ console.log('Message ID:', response.message_id);
209
+ ```
210
+
211
+ **With custom code length:**
212
+
213
+ ```typescript
214
+ const response = await client.otp.send({
215
+ number: '639171234567',
216
+ message: 'Your code: {otp}',
217
+ sendername: 'MyApp',
218
+ code_length: 4, // Generate 4-digit code instead of default 6
219
+ });
220
+ ```
221
+
222
+ **Request Parameters:**
223
+
224
+ | Parameter | Type | Required | Description |
225
+ |-----------|------|----------|-------------|
226
+ | `number` | `string` | Yes | Recipient phone number |
227
+ | `message` | `string` | Yes | Message template with `{otp}` placeholder |
228
+ | `sendername` | `string` | Yes | Approved sender name |
229
+ | `code_length` | `number` | No | OTP code length (default: 6) |
230
+
231
+ **Response:**
232
+
233
+ ```typescript
234
+ interface SendOtpResponse {
235
+ message_id: number;
236
+ code: string; // The generated OTP code
237
+ user_id: number;
238
+ user: string;
239
+ account_id: number;
240
+ account: string;
241
+ recipient: string;
242
+ message: string;
243
+ sender_name: string;
244
+ network: string;
245
+ status: string;
246
+ type: string;
247
+ source: string;
248
+ created_at: string;
249
+ updated_at: string;
250
+ }
251
+ ```
252
+
253
+ ---
254
+
255
+ ### Account (`client.account`)
256
+
257
+ #### `info()`
258
+
259
+ Get account information including credit balance.
260
+
261
+ ```typescript
262
+ const info = await client.account.info();
263
+
264
+ console.log('Account:', info.account_name);
265
+ console.log('Status:', info.status);
266
+ console.log('Balance:', info.credit_balance);
267
+ ```
268
+
269
+ **Response:**
270
+
271
+ ```typescript
272
+ interface AccountInfo {
273
+ account_id: number;
274
+ account_name: string;
275
+ status: string;
276
+ credit_balance: string;
277
+ }
278
+ ```
279
+
280
+ #### `transactions(params?)`
281
+
282
+ Get transaction history with optional filtering.
283
+
284
+ ```typescript
285
+ // Get all transactions
286
+ const transactions = await client.account.transactions();
287
+
288
+ // Get with pagination and date range
289
+ const filtered = await client.account.transactions({
290
+ page: 1,
291
+ limit: 50,
292
+ startDate: '2024-01-01',
293
+ endDate: '2024-01-31',
294
+ });
295
+
296
+ for (const tx of filtered) {
297
+ console.log(`${tx.type}: ${tx.amount} credits - ${tx.description}`);
298
+ }
299
+ ```
300
+
301
+ **Filter Parameters:**
302
+
303
+ | Parameter | Type | Description |
304
+ |-----------|------|-------------|
305
+ | `page` | `number` | Page number for pagination |
306
+ | `limit` | `number` | Number of results per page |
307
+ | `startDate` | `string` | Filter by start date (YYYY-MM-DD) |
308
+ | `endDate` | `string` | Filter by end date (YYYY-MM-DD) |
309
+
310
+ **Response:**
311
+
312
+ ```typescript
313
+ interface Transaction {
314
+ id: number;
315
+ account_id: number;
316
+ user_id: number;
317
+ type: string;
318
+ amount: string;
319
+ balance_before: string;
320
+ balance_after: string;
321
+ description: string;
322
+ created_at: string;
323
+ }
324
+ ```
325
+
326
+ #### `senderNames()`
327
+
328
+ Get list of approved sender names.
329
+
330
+ ```typescript
331
+ const senderNames = await client.account.senderNames();
332
+
333
+ for (const sender of senderNames) {
334
+ console.log(`${sender.name}: ${sender.status}`);
335
+ }
336
+ ```
337
+
338
+ **Response:**
339
+
340
+ ```typescript
341
+ interface SenderName {
342
+ name: string;
343
+ status: string;
344
+ created_at: string;
345
+ }
346
+ ```
347
+
348
+ ## Error Handling
349
+
350
+ The library throws typed errors for different failure scenarios:
351
+
352
+ ```typescript
353
+ import {
354
+ SemaphoreClient,
355
+ SemaphoreError,
356
+ AuthenticationError,
357
+ RateLimitError,
358
+ ValidationError,
359
+ NetworkError,
360
+ TimeoutError,
361
+ } from 'semaphoreco';
362
+
363
+ const client = new SemaphoreClient({ apiKey: 'your-api-key' });
364
+
365
+ try {
366
+ await client.messages.send({
367
+ number: '639171234567',
368
+ message: 'Hello!',
369
+ sendername: 'SEMAPHORE',
370
+ });
371
+ } catch (error) {
372
+ if (error instanceof RateLimitError) {
373
+ // Rate limited (429) - retry after delay
374
+ console.log('Rate limited, retry after:', error.retryAfter, 'seconds');
375
+ } else if (error instanceof AuthenticationError) {
376
+ // Invalid API key (401)
377
+ console.log('Invalid API key');
378
+ } else if (error instanceof ValidationError) {
379
+ // Invalid request parameters (400)
380
+ console.log('Validation error:', error.message);
381
+ console.log('Field:', error.field);
382
+ } else if (error instanceof TimeoutError) {
383
+ // Request timed out
384
+ console.log('Request timed out after:', error.timeoutMs, 'ms');
385
+ } else if (error instanceof NetworkError) {
386
+ // Network connection failed
387
+ console.log('Network error:', error.message);
388
+ console.log('Cause:', error.cause);
389
+ } else if (error instanceof SemaphoreError) {
390
+ // Other API errors (5xx, etc.)
391
+ console.log('API error:', error.message);
392
+ console.log('Status:', error.statusCode);
393
+ }
394
+ }
395
+ ```
396
+
397
+ ### Error Types
398
+
399
+ | Error | Status Code | Description |
400
+ |-------|-------------|-------------|
401
+ | `AuthenticationError` | 401 | Invalid or missing API key |
402
+ | `RateLimitError` | 429 | Rate limit exceeded (120 req/min for standard SMS) |
403
+ | `ValidationError` | 400 | Invalid request parameters |
404
+ | `NetworkError` | - | Connection failure, DNS error |
405
+ | `TimeoutError` | - | Request exceeded timeout |
406
+ | `SemaphoreError` | 5xx | Server errors and other failures |
407
+
408
+ ### Automatic Retry
409
+
410
+ The client automatically retries rate-limited requests (429 responses) with exponential backoff. You can track retries with the `onRetry` callback:
411
+
412
+ ```typescript
413
+ const client = new SemaphoreClient({
414
+ apiKey: 'your-api-key',
415
+ onRetry: (attempt, delayMs) => {
416
+ console.log(`Rate limited. Retry attempt ${attempt} in ${delayMs}ms`);
417
+ },
418
+ });
419
+ ```
420
+
421
+ ## SMS Credits
422
+
423
+ - **Standard SMS**: 1 credit per 160 ASCII characters
424
+ - **OTP SMS**: 2 credits per 160 ASCII characters (priority route)
425
+ - **Long messages**: Split into 153-character segments
426
+ - **Unicode**: Limited to 70 characters per segment
427
+
428
+ ## Requirements
429
+
430
+ - Node.js 18.0.0 or later (uses native `fetch`)
431
+ - Also works with Deno, Bun, and other modern JavaScript runtimes
432
+
433
+ ## License
434
+
435
+ MIT