@textbee/sdk 0.2.0 → 0.3.1
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 +31 -7
- package/dist/index.cjs +56 -11
- package/dist/index.d.cts +63 -10
- package/dist/index.d.ts +63 -10
- package/dist/index.js +56 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Official JavaScript SDK for [textbee.dev](https://textbee.dev), the open source
|
|
|
8
8
|
|
|
9
9
|
Zero dependencies, TypeScript types included, works in Node 18+, Bun, Deno, Cloudflare Workers, and Vercel Edge.
|
|
10
10
|
|
|
11
|
+
Writing Python? Use the official [Python SDK](https://github.com/textbee/textbee-python): `pip install textbee`.
|
|
12
|
+
|
|
11
13
|
## Install
|
|
12
14
|
|
|
13
15
|
```bash
|
|
@@ -41,7 +43,7 @@ await textbee.sendSms({
|
|
|
41
43
|
| --- | --- | --- |
|
|
42
44
|
| `deviceId` | `string` | Which phone sends the message. Omit it and textbee uses your default device, or the enabled device with the most recent heartbeat. |
|
|
43
45
|
| `simSubscriptionId` | `number` | Which SIM sends the message on a multi-SIM phone. Omit it and the phone uses its configured preferred SIM, or the system default. |
|
|
44
|
-
| `scheduledAt` | `string \| Date` | Send later instead of now. ISO 8601 or a `Date`, must be in the future
|
|
46
|
+
| `scheduledAt` | `string \| Date` | Send later instead of now. ISO 8601 or a `Date`, must be in the future. |
|
|
45
47
|
|
|
46
48
|
```js
|
|
47
49
|
await textbee.sendSms({
|
|
@@ -76,9 +78,9 @@ History is account-level: one call covers every device, and `deviceIds` narrows
|
|
|
76
78
|
```js
|
|
77
79
|
// Paginated history across the whole account, filterable and searchable
|
|
78
80
|
const { data, meta } = await textbee.getMessages({
|
|
79
|
-
direction: '
|
|
81
|
+
direction: 'sent', // 'all' | 'sent' | 'received'
|
|
80
82
|
deviceIds: [deviceId], // omit for every device
|
|
81
|
-
status: 'delivered', // delivery state;
|
|
83
|
+
status: 'delivered', // delivery state; status=failed lists failed sends
|
|
82
84
|
search: 'invoice',
|
|
83
85
|
from: '2026-08-01', // dates are UTC; datetimes need an explicit timezone
|
|
84
86
|
to: '2026-09-01T00:00:00Z', // exclusive, so windows never double-count
|
|
@@ -87,7 +89,7 @@ const { data, meta } = await textbee.getMessages({
|
|
|
87
89
|
})
|
|
88
90
|
|
|
89
91
|
// direction on each message is lowercase and feeds straight back into filters
|
|
90
|
-
data.filter((m) => m.direction === '
|
|
92
|
+
data.filter((m) => m.direction === 'sent')
|
|
91
93
|
|
|
92
94
|
// Which recipients of a bulk send failed: filter by the batch a send returned
|
|
93
95
|
const { smsBatchId } = await textbee.sendSms({ recipients, message })
|
|
@@ -102,10 +104,16 @@ for await (const message of textbee.iterateMessages({ direction: 'received', ord
|
|
|
102
104
|
// A single message and its current status
|
|
103
105
|
const sms = await textbee.getSms(deviceId, smsId)
|
|
104
106
|
|
|
105
|
-
// A whole batch, using the smsBatchId returned by sendSms
|
|
107
|
+
// A whole batch, using the smsBatchId returned by sendSms. This needs the id
|
|
108
|
+
// of the device that sent it; when textbee picked the device, use
|
|
109
|
+
// getMessages({ smsBatchId }) instead.
|
|
106
110
|
const { batch, messages } = await textbee.getSmsBatch(deviceId, smsBatchId)
|
|
107
111
|
```
|
|
108
112
|
|
|
113
|
+
`getSms` and `getSmsBatch` return the `Sms` type. It differs from the `Message` that `getMessages` returns: `device` is the device id string, not an object with the phone's name and model, and `direction` is absent (read `type`).
|
|
114
|
+
|
|
115
|
+
`batch.status` and the batch counts come from the messages in the batch, so they change as each message reports. `successCount` counts messages sent or delivered, and `failureCount` counts failed ones. `pendingCount`, `dispatchedCount`, `sentCount`, `deliveredCount` and `unknownCount` break the rest down by state.
|
|
116
|
+
|
|
109
117
|
## Verifying webhooks
|
|
110
118
|
|
|
111
119
|
textbee signs each webhook delivery with HMAC-SHA256 and sends the hex digest in the `X-Signature` header. Pass the raw request body, not a re-serialized object, whenever your framework gives you access to it.
|
|
@@ -209,14 +217,30 @@ try {
|
|
|
209
217
|
```js
|
|
210
218
|
new Textbee({
|
|
211
219
|
apiKey: 'your-api-key',
|
|
212
|
-
baseUrl: 'https://api.textbee.dev/api/v1', //
|
|
220
|
+
baseUrl: 'https://api.textbee.dev/api/v1', // the default
|
|
213
221
|
})
|
|
214
222
|
```
|
|
215
223
|
|
|
224
|
+
### Self-hosted textbee
|
|
225
|
+
|
|
226
|
+
Point `baseUrl` at your own instance. The `/api/v1` suffix is optional, so all of these reach the same API:
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
new Textbee({ apiKey, baseUrl: 'https://sms.example.com' })
|
|
230
|
+
new Textbee({ apiKey, baseUrl: 'https://sms.example.com/api/v1' })
|
|
231
|
+
new Textbee({ apiKey, baseUrl: 'http://localhost:3001' })
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If the API sits under a path prefix, give the prefix: `https://example.com/textbee` reaches `https://example.com/textbee/api/v1`. The SDK adds `/api/v1` unless the path already contains it, the same rule the textbee MCP server uses for `TEXTBEE_BASE_URL`. A value with no scheme gets `https://`. An invalid URL, or one with a username or password, throws a `TypeError` instead of falling back to the hosted API, so a typo never sends your key to textbee.dev.
|
|
235
|
+
|
|
216
236
|
## What is not covered yet
|
|
217
237
|
|
|
218
238
|
The SDK focuses on sending and reading messages. Bulk send and a few device operations are still REST only, documented at [textbee.dev/docs](https://textbee.dev/docs).
|
|
219
239
|
|
|
240
|
+
## Community and support
|
|
241
|
+
|
|
242
|
+
Questions or feedback? Join the community on [Discord](https://textbee.dev/discord) or email support@textbee.dev.
|
|
243
|
+
|
|
220
244
|
## License
|
|
221
245
|
|
|
222
|
-
MIT. Part of the [textbee](https://github.com/
|
|
246
|
+
MIT. Part of the [textbee](https://github.com/textbee/textbee) project.
|
package/dist/index.cjs
CHANGED
|
@@ -48,7 +48,7 @@ var TextbeeError = class extends Error {
|
|
|
48
48
|
|
|
49
49
|
// src/client.ts
|
|
50
50
|
var DEFAULT_BASE_URL = "https://api.textbee.dev/api/v1";
|
|
51
|
-
var CLIENT_ID = `textbee-js/${"0.
|
|
51
|
+
var CLIENT_ID = `textbee-js/${"0.3.1"}`;
|
|
52
52
|
var Textbee = class {
|
|
53
53
|
#apiKey;
|
|
54
54
|
#baseUrl;
|
|
@@ -57,7 +57,7 @@ var Textbee = class {
|
|
|
57
57
|
throw new TypeError("Textbee requires an apiKey");
|
|
58
58
|
}
|
|
59
59
|
this.#apiKey = options.apiKey;
|
|
60
|
-
this.#baseUrl = (options.baseUrl
|
|
60
|
+
this.#baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
61
61
|
}
|
|
62
62
|
/** Send an SMS to one or more recipients. */
|
|
63
63
|
async sendSms(request) {
|
|
@@ -128,9 +128,10 @@ var Textbee = class {
|
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
130
|
* Iterate every message matching the filters, following the pagination
|
|
131
|
-
* cursor until the end.
|
|
132
|
-
*
|
|
133
|
-
* `
|
|
131
|
+
* cursor until the end. `limit` is the page size, not a cap on the total.
|
|
132
|
+
* Use `order: 'asc'` with a `from` bound to drain forward when polling.
|
|
133
|
+
* `from` is inclusive, so a poll that resumes from the last `createdAt` sees
|
|
134
|
+
* that message again; skip ids you have already handled.
|
|
134
135
|
*
|
|
135
136
|
* ```ts
|
|
136
137
|
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
@@ -154,7 +155,11 @@ var Textbee = class {
|
|
|
154
155
|
);
|
|
155
156
|
return payload.data;
|
|
156
157
|
}
|
|
157
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Fetch a batch and every message in it, by the `smsBatchId` a send returns.
|
|
160
|
+
* Needs the id of the device that sent the batch. When textbee picked the
|
|
161
|
+
* device, use `getMessages({ smsBatchId })` instead.
|
|
162
|
+
*/
|
|
158
163
|
async getSmsBatch(deviceId, smsBatchId) {
|
|
159
164
|
const payload = await this.#request(
|
|
160
165
|
"GET",
|
|
@@ -190,6 +195,42 @@ var Textbee = class {
|
|
|
190
195
|
return payload;
|
|
191
196
|
}
|
|
192
197
|
};
|
|
198
|
+
function normalizeBaseUrl(raw) {
|
|
199
|
+
const value = raw?.trim();
|
|
200
|
+
if (!value) {
|
|
201
|
+
return DEFAULT_BASE_URL;
|
|
202
|
+
}
|
|
203
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
|
|
204
|
+
let url;
|
|
205
|
+
try {
|
|
206
|
+
url = new URL(withScheme);
|
|
207
|
+
} catch {
|
|
208
|
+
throw new TypeError(
|
|
209
|
+
"baseUrl is not a valid URL. Use your textbee api, for example https://sms.example.com. The /api/v1 suffix is added for you."
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
213
|
+
throw new TypeError(`baseUrl must be http or https, got "${url.protocol}"`);
|
|
214
|
+
}
|
|
215
|
+
if (url.username || url.password) {
|
|
216
|
+
throw new TypeError("baseUrl must not contain a username or password");
|
|
217
|
+
}
|
|
218
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
219
|
+
let cut = -1;
|
|
220
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
221
|
+
if (segments[i].toLowerCase() === "api" && segments[i + 1].toLowerCase() === "v1") {
|
|
222
|
+
cut = i + 2;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let path;
|
|
227
|
+
if (cut === -1) {
|
|
228
|
+
path = [...segments, "api", "v1"];
|
|
229
|
+
} else {
|
|
230
|
+
path = [...segments.slice(0, cut - 2), "api", "v1"];
|
|
231
|
+
}
|
|
232
|
+
return [url.origin, ...path].join("/");
|
|
233
|
+
}
|
|
193
234
|
function toIsoString(value) {
|
|
194
235
|
return value instanceof Date ? value.toISOString() : value;
|
|
195
236
|
}
|
|
@@ -207,11 +248,15 @@ async function readBody(response) {
|
|
|
207
248
|
function errorMessage(payload, status) {
|
|
208
249
|
if (payload && typeof payload === "object") {
|
|
209
250
|
const record = payload;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
251
|
+
const keys = "statusCode" in record ? ["message", "error"] : ["error", "message"];
|
|
252
|
+
for (const key of keys) {
|
|
253
|
+
const value = record[key];
|
|
254
|
+
if (typeof value === "string" && value) {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string")) {
|
|
258
|
+
return value.join("; ");
|
|
259
|
+
}
|
|
215
260
|
}
|
|
216
261
|
}
|
|
217
262
|
return `HTTP ${status}`;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
interface TextbeeOptions {
|
|
2
2
|
/** API key from the textbee dashboard. Sent as the `x-api-key` header. */
|
|
3
3
|
apiKey: string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Your self-hosted textbee API. Defaults to `https://api.textbee.dev/api/v1`.
|
|
6
|
+
*
|
|
7
|
+
* `/api/v1` is added unless the path already contains it, so
|
|
8
|
+
* `https://sms.example.com` and `https://example.com/textbee` both work. A URL
|
|
9
|
+
* that contains `/api/v1` is cut there, so a pasted endpoint URL still works.
|
|
10
|
+
* A value with no scheme gets `https://`. An invalid URL throws a `TypeError`
|
|
11
|
+
* rather than falling back, so a typo never sends your key to the public API.
|
|
12
|
+
*/
|
|
5
13
|
baseUrl?: string;
|
|
6
14
|
}
|
|
7
15
|
interface SendSmsRequest {
|
|
@@ -33,20 +41,32 @@ interface SendSmsRequest {
|
|
|
33
41
|
/**
|
|
34
42
|
* Send later instead of now, as an ISO 8601 string or a `Date`.
|
|
35
43
|
*
|
|
36
|
-
* Must be in the future
|
|
44
|
+
* Must be in the future. Omit for an immediate send.
|
|
37
45
|
*/
|
|
38
46
|
scheduledAt?: string | Date;
|
|
39
47
|
}
|
|
48
|
+
/** A reason the send will probably fail, such as `SMS_PERMISSION_MISSING`. */
|
|
49
|
+
interface SendSmsWarning {
|
|
50
|
+
code: string;
|
|
51
|
+
message: string;
|
|
52
|
+
}
|
|
40
53
|
/** Returned when the message was accepted for delivery and queued. */
|
|
41
54
|
interface SendSmsQueuedResponse {
|
|
42
55
|
success: true;
|
|
43
56
|
message: string;
|
|
44
57
|
smsBatchId: string;
|
|
45
58
|
recipientCount: number;
|
|
59
|
+
/** When a large send, spread over several waves, should finish. */
|
|
60
|
+
estimatedCompletionAt?: string;
|
|
61
|
+
/** Present when the phone last reported a problem, such as the SMS permission off. */
|
|
62
|
+
warning?: SendSmsWarning;
|
|
46
63
|
}
|
|
47
64
|
/** Returned when the message was dispatched to the device immediately. */
|
|
48
65
|
interface SendSmsImmediateResponse {
|
|
49
66
|
successCount: number;
|
|
67
|
+
failureCount?: number;
|
|
68
|
+
/** Present when the phone last reported a problem, such as the SMS permission off. */
|
|
69
|
+
warning?: SendSmsWarning;
|
|
50
70
|
[key: string]: unknown;
|
|
51
71
|
}
|
|
52
72
|
/**
|
|
@@ -110,6 +130,7 @@ interface Device {
|
|
|
110
130
|
}
|
|
111
131
|
interface MessageDevice {
|
|
112
132
|
_id: string;
|
|
133
|
+
name?: string;
|
|
113
134
|
brand?: string;
|
|
114
135
|
model?: string;
|
|
115
136
|
buildId?: string;
|
|
@@ -129,7 +150,7 @@ interface Message {
|
|
|
129
150
|
direction?: 'sent' | 'received';
|
|
130
151
|
/** Message channel. Currently always `sms`; absent means `sms`. */
|
|
131
152
|
channel?: string;
|
|
132
|
-
/** The sending or receiving device
|
|
153
|
+
/** The sending or receiving device. */
|
|
133
154
|
device?: MessageDevice;
|
|
134
155
|
/** Lowercase, and absent on messages stored before status tracking. */
|
|
135
156
|
status?: 'pending' | 'dispatched' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received';
|
|
@@ -148,16 +169,35 @@ interface Message {
|
|
|
148
169
|
createdAt?: string;
|
|
149
170
|
updatedAt?: string;
|
|
150
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* A send batch. The status and counts are calculated from the batch's
|
|
174
|
+
* messages, so they change as each message reports.
|
|
175
|
+
*/
|
|
151
176
|
interface SmsBatch {
|
|
152
177
|
_id: string;
|
|
153
178
|
message: string;
|
|
154
179
|
recipientCount: number;
|
|
155
180
|
recipientPreview?: string;
|
|
181
|
+
/** Messages sent or delivered: `sentCount + deliveredCount`. */
|
|
156
182
|
successCount: number;
|
|
183
|
+
/** Messages that failed. */
|
|
157
184
|
failureCount: number;
|
|
185
|
+
/** Messages not yet handed to the phone. */
|
|
186
|
+
pendingCount?: number;
|
|
187
|
+
/** Messages handed to the phone with no result yet. */
|
|
188
|
+
dispatchedCount?: number;
|
|
189
|
+
/** Messages sent with no delivery report yet. */
|
|
190
|
+
sentCount?: number;
|
|
191
|
+
/** Messages with a delivery report. */
|
|
192
|
+
deliveredCount?: number;
|
|
193
|
+
/** Messages with no report from the phone within 20 minutes. */
|
|
194
|
+
unknownCount?: number;
|
|
158
195
|
status: 'pending' | 'processing' | 'completed' | 'partial_success' | 'failed' | 'unknown';
|
|
159
196
|
error?: string;
|
|
197
|
+
/** When the batch reached its current final status. Absent while processing. */
|
|
160
198
|
completedAt?: string;
|
|
199
|
+
/** When the status and counts were last calculated. */
|
|
200
|
+
statusCheckedAt?: string;
|
|
161
201
|
createdAt?: string;
|
|
162
202
|
updatedAt?: string;
|
|
163
203
|
}
|
|
@@ -175,13 +215,21 @@ interface MessageListMeta {
|
|
|
175
215
|
nextCursor?: string | null;
|
|
176
216
|
hasMore?: boolean;
|
|
177
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* A message as `getSms` and `getSmsBatch` return it: `device` is the id
|
|
220
|
+
* string, and `direction` is absent (read `type`).
|
|
221
|
+
*/
|
|
222
|
+
interface Sms extends Omit<Message, 'device' | 'direction'> {
|
|
223
|
+
/** The id of the sending or receiving device. */
|
|
224
|
+
device?: string;
|
|
225
|
+
}
|
|
178
226
|
interface MessageList {
|
|
179
227
|
data: Message[];
|
|
180
228
|
meta: MessageListMeta;
|
|
181
229
|
}
|
|
182
230
|
interface SmsBatchResult {
|
|
183
231
|
batch: SmsBatch;
|
|
184
|
-
messages:
|
|
232
|
+
messages: Sms[];
|
|
185
233
|
}
|
|
186
234
|
/** Event names textbee sends in a webhook payload. */
|
|
187
235
|
type WebhookEvent = 'MESSAGE_RECEIVED' | 'MESSAGE_SENT' | 'MESSAGE_DELIVERED' | 'MESSAGE_FAILED' | 'UNKNOWN_STATE' | 'SMS_STATUS_UPDATED';
|
|
@@ -204,9 +252,10 @@ declare class Textbee {
|
|
|
204
252
|
getMessages(options?: ListMessagesOptions): Promise<MessageList>;
|
|
205
253
|
/**
|
|
206
254
|
* Iterate every message matching the filters, following the pagination
|
|
207
|
-
* cursor until the end.
|
|
208
|
-
*
|
|
209
|
-
* `
|
|
255
|
+
* cursor until the end. `limit` is the page size, not a cap on the total.
|
|
256
|
+
* Use `order: 'asc'` with a `from` bound to drain forward when polling.
|
|
257
|
+
* `from` is inclusive, so a poll that resumes from the last `createdAt` sees
|
|
258
|
+
* that message again; skip ids you have already handled.
|
|
210
259
|
*
|
|
211
260
|
* ```ts
|
|
212
261
|
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
@@ -216,8 +265,12 @@ declare class Textbee {
|
|
|
216
265
|
*/
|
|
217
266
|
iterateMessages(options?: IterateMessagesOptions): AsyncGenerator<Message, void, undefined>;
|
|
218
267
|
/** Fetch a single message, including its delivery status. */
|
|
219
|
-
getSms(deviceId: string, smsId: string): Promise<
|
|
220
|
-
/**
|
|
268
|
+
getSms(deviceId: string, smsId: string): Promise<Sms>;
|
|
269
|
+
/**
|
|
270
|
+
* Fetch a batch and every message in it, by the `smsBatchId` a send returns.
|
|
271
|
+
* Needs the id of the device that sent the batch. When textbee picked the
|
|
272
|
+
* device, use `getMessages({ smsBatchId })` instead.
|
|
273
|
+
*/
|
|
221
274
|
getSmsBatch(deviceId: string, smsBatchId: string): Promise<SmsBatchResult>;
|
|
222
275
|
}
|
|
223
276
|
|
|
@@ -347,4 +400,4 @@ interface VerifyWebhookSignatureOptions {
|
|
|
347
400
|
*/
|
|
348
401
|
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
349
402
|
|
|
350
|
-
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type NormalizePhoneNumberOptions, type SanitizeForGsm7Options, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SmsBatch, type SmsBatchResult, type SmsEncoding, type SmsSegmentInfo, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, countSmsSegments, findNonGsm7Characters, getSmsEncoding, isValidE164, normalizePhoneNumber, sanitizeForGsm7, verifyWebhookSignature };
|
|
403
|
+
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type NormalizePhoneNumberOptions, type SanitizeForGsm7Options, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SendSmsWarning, type Sms, type SmsBatch, type SmsBatchResult, type SmsEncoding, type SmsSegmentInfo, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, countSmsSegments, findNonGsm7Characters, getSmsEncoding, isValidE164, normalizePhoneNumber, sanitizeForGsm7, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
interface TextbeeOptions {
|
|
2
2
|
/** API key from the textbee dashboard. Sent as the `x-api-key` header. */
|
|
3
3
|
apiKey: string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Your self-hosted textbee API. Defaults to `https://api.textbee.dev/api/v1`.
|
|
6
|
+
*
|
|
7
|
+
* `/api/v1` is added unless the path already contains it, so
|
|
8
|
+
* `https://sms.example.com` and `https://example.com/textbee` both work. A URL
|
|
9
|
+
* that contains `/api/v1` is cut there, so a pasted endpoint URL still works.
|
|
10
|
+
* A value with no scheme gets `https://`. An invalid URL throws a `TypeError`
|
|
11
|
+
* rather than falling back, so a typo never sends your key to the public API.
|
|
12
|
+
*/
|
|
5
13
|
baseUrl?: string;
|
|
6
14
|
}
|
|
7
15
|
interface SendSmsRequest {
|
|
@@ -33,20 +41,32 @@ interface SendSmsRequest {
|
|
|
33
41
|
/**
|
|
34
42
|
* Send later instead of now, as an ISO 8601 string or a `Date`.
|
|
35
43
|
*
|
|
36
|
-
* Must be in the future
|
|
44
|
+
* Must be in the future. Omit for an immediate send.
|
|
37
45
|
*/
|
|
38
46
|
scheduledAt?: string | Date;
|
|
39
47
|
}
|
|
48
|
+
/** A reason the send will probably fail, such as `SMS_PERMISSION_MISSING`. */
|
|
49
|
+
interface SendSmsWarning {
|
|
50
|
+
code: string;
|
|
51
|
+
message: string;
|
|
52
|
+
}
|
|
40
53
|
/** Returned when the message was accepted for delivery and queued. */
|
|
41
54
|
interface SendSmsQueuedResponse {
|
|
42
55
|
success: true;
|
|
43
56
|
message: string;
|
|
44
57
|
smsBatchId: string;
|
|
45
58
|
recipientCount: number;
|
|
59
|
+
/** When a large send, spread over several waves, should finish. */
|
|
60
|
+
estimatedCompletionAt?: string;
|
|
61
|
+
/** Present when the phone last reported a problem, such as the SMS permission off. */
|
|
62
|
+
warning?: SendSmsWarning;
|
|
46
63
|
}
|
|
47
64
|
/** Returned when the message was dispatched to the device immediately. */
|
|
48
65
|
interface SendSmsImmediateResponse {
|
|
49
66
|
successCount: number;
|
|
67
|
+
failureCount?: number;
|
|
68
|
+
/** Present when the phone last reported a problem, such as the SMS permission off. */
|
|
69
|
+
warning?: SendSmsWarning;
|
|
50
70
|
[key: string]: unknown;
|
|
51
71
|
}
|
|
52
72
|
/**
|
|
@@ -110,6 +130,7 @@ interface Device {
|
|
|
110
130
|
}
|
|
111
131
|
interface MessageDevice {
|
|
112
132
|
_id: string;
|
|
133
|
+
name?: string;
|
|
113
134
|
brand?: string;
|
|
114
135
|
model?: string;
|
|
115
136
|
buildId?: string;
|
|
@@ -129,7 +150,7 @@ interface Message {
|
|
|
129
150
|
direction?: 'sent' | 'received';
|
|
130
151
|
/** Message channel. Currently always `sms`; absent means `sms`. */
|
|
131
152
|
channel?: string;
|
|
132
|
-
/** The sending or receiving device
|
|
153
|
+
/** The sending or receiving device. */
|
|
133
154
|
device?: MessageDevice;
|
|
134
155
|
/** Lowercase, and absent on messages stored before status tracking. */
|
|
135
156
|
status?: 'pending' | 'dispatched' | 'sent' | 'delivered' | 'failed' | 'unknown' | 'received';
|
|
@@ -148,16 +169,35 @@ interface Message {
|
|
|
148
169
|
createdAt?: string;
|
|
149
170
|
updatedAt?: string;
|
|
150
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* A send batch. The status and counts are calculated from the batch's
|
|
174
|
+
* messages, so they change as each message reports.
|
|
175
|
+
*/
|
|
151
176
|
interface SmsBatch {
|
|
152
177
|
_id: string;
|
|
153
178
|
message: string;
|
|
154
179
|
recipientCount: number;
|
|
155
180
|
recipientPreview?: string;
|
|
181
|
+
/** Messages sent or delivered: `sentCount + deliveredCount`. */
|
|
156
182
|
successCount: number;
|
|
183
|
+
/** Messages that failed. */
|
|
157
184
|
failureCount: number;
|
|
185
|
+
/** Messages not yet handed to the phone. */
|
|
186
|
+
pendingCount?: number;
|
|
187
|
+
/** Messages handed to the phone with no result yet. */
|
|
188
|
+
dispatchedCount?: number;
|
|
189
|
+
/** Messages sent with no delivery report yet. */
|
|
190
|
+
sentCount?: number;
|
|
191
|
+
/** Messages with a delivery report. */
|
|
192
|
+
deliveredCount?: number;
|
|
193
|
+
/** Messages with no report from the phone within 20 minutes. */
|
|
194
|
+
unknownCount?: number;
|
|
158
195
|
status: 'pending' | 'processing' | 'completed' | 'partial_success' | 'failed' | 'unknown';
|
|
159
196
|
error?: string;
|
|
197
|
+
/** When the batch reached its current final status. Absent while processing. */
|
|
160
198
|
completedAt?: string;
|
|
199
|
+
/** When the status and counts were last calculated. */
|
|
200
|
+
statusCheckedAt?: string;
|
|
161
201
|
createdAt?: string;
|
|
162
202
|
updatedAt?: string;
|
|
163
203
|
}
|
|
@@ -175,13 +215,21 @@ interface MessageListMeta {
|
|
|
175
215
|
nextCursor?: string | null;
|
|
176
216
|
hasMore?: boolean;
|
|
177
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* A message as `getSms` and `getSmsBatch` return it: `device` is the id
|
|
220
|
+
* string, and `direction` is absent (read `type`).
|
|
221
|
+
*/
|
|
222
|
+
interface Sms extends Omit<Message, 'device' | 'direction'> {
|
|
223
|
+
/** The id of the sending or receiving device. */
|
|
224
|
+
device?: string;
|
|
225
|
+
}
|
|
178
226
|
interface MessageList {
|
|
179
227
|
data: Message[];
|
|
180
228
|
meta: MessageListMeta;
|
|
181
229
|
}
|
|
182
230
|
interface SmsBatchResult {
|
|
183
231
|
batch: SmsBatch;
|
|
184
|
-
messages:
|
|
232
|
+
messages: Sms[];
|
|
185
233
|
}
|
|
186
234
|
/** Event names textbee sends in a webhook payload. */
|
|
187
235
|
type WebhookEvent = 'MESSAGE_RECEIVED' | 'MESSAGE_SENT' | 'MESSAGE_DELIVERED' | 'MESSAGE_FAILED' | 'UNKNOWN_STATE' | 'SMS_STATUS_UPDATED';
|
|
@@ -204,9 +252,10 @@ declare class Textbee {
|
|
|
204
252
|
getMessages(options?: ListMessagesOptions): Promise<MessageList>;
|
|
205
253
|
/**
|
|
206
254
|
* Iterate every message matching the filters, following the pagination
|
|
207
|
-
* cursor until the end.
|
|
208
|
-
*
|
|
209
|
-
* `
|
|
255
|
+
* cursor until the end. `limit` is the page size, not a cap on the total.
|
|
256
|
+
* Use `order: 'asc'` with a `from` bound to drain forward when polling.
|
|
257
|
+
* `from` is inclusive, so a poll that resumes from the last `createdAt` sees
|
|
258
|
+
* that message again; skip ids you have already handled.
|
|
210
259
|
*
|
|
211
260
|
* ```ts
|
|
212
261
|
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
@@ -216,8 +265,12 @@ declare class Textbee {
|
|
|
216
265
|
*/
|
|
217
266
|
iterateMessages(options?: IterateMessagesOptions): AsyncGenerator<Message, void, undefined>;
|
|
218
267
|
/** Fetch a single message, including its delivery status. */
|
|
219
|
-
getSms(deviceId: string, smsId: string): Promise<
|
|
220
|
-
/**
|
|
268
|
+
getSms(deviceId: string, smsId: string): Promise<Sms>;
|
|
269
|
+
/**
|
|
270
|
+
* Fetch a batch and every message in it, by the `smsBatchId` a send returns.
|
|
271
|
+
* Needs the id of the device that sent the batch. When textbee picked the
|
|
272
|
+
* device, use `getMessages({ smsBatchId })` instead.
|
|
273
|
+
*/
|
|
221
274
|
getSmsBatch(deviceId: string, smsBatchId: string): Promise<SmsBatchResult>;
|
|
222
275
|
}
|
|
223
276
|
|
|
@@ -347,4 +400,4 @@ interface VerifyWebhookSignatureOptions {
|
|
|
347
400
|
*/
|
|
348
401
|
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<boolean>;
|
|
349
402
|
|
|
350
|
-
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type NormalizePhoneNumberOptions, type SanitizeForGsm7Options, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SmsBatch, type SmsBatchResult, type SmsEncoding, type SmsSegmentInfo, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, countSmsSegments, findNonGsm7Characters, getSmsEncoding, isValidE164, normalizePhoneNumber, sanitizeForGsm7, verifyWebhookSignature };
|
|
403
|
+
export { type Device, type IterateMessagesOptions, type ListMessagesOptions, type Message, type MessageDevice, type MessageList, type MessageListMeta, type NormalizePhoneNumberOptions, type SanitizeForGsm7Options, type SendSmsImmediateResponse, type SendSmsQueuedResponse, type SendSmsRequest, type SendSmsResponse, type SendSmsWarning, type Sms, type SmsBatch, type SmsBatchResult, type SmsEncoding, type SmsSegmentInfo, Textbee, TextbeeError, type TextbeeOptions, type VerifyWebhookSignatureOptions, type WebhookEvent, countSmsSegments, findNonGsm7Characters, getSmsEncoding, isValidE164, normalizePhoneNumber, sanitizeForGsm7, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var TextbeeError = class extends Error {
|
|
|
14
14
|
|
|
15
15
|
// src/client.ts
|
|
16
16
|
var DEFAULT_BASE_URL = "https://api.textbee.dev/api/v1";
|
|
17
|
-
var CLIENT_ID = `textbee-js/${"0.
|
|
17
|
+
var CLIENT_ID = `textbee-js/${"0.3.1"}`;
|
|
18
18
|
var Textbee = class {
|
|
19
19
|
#apiKey;
|
|
20
20
|
#baseUrl;
|
|
@@ -23,7 +23,7 @@ var Textbee = class {
|
|
|
23
23
|
throw new TypeError("Textbee requires an apiKey");
|
|
24
24
|
}
|
|
25
25
|
this.#apiKey = options.apiKey;
|
|
26
|
-
this.#baseUrl = (options.baseUrl
|
|
26
|
+
this.#baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
27
27
|
}
|
|
28
28
|
/** Send an SMS to one or more recipients. */
|
|
29
29
|
async sendSms(request) {
|
|
@@ -94,9 +94,10 @@ var Textbee = class {
|
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Iterate every message matching the filters, following the pagination
|
|
97
|
-
* cursor until the end.
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
97
|
+
* cursor until the end. `limit` is the page size, not a cap on the total.
|
|
98
|
+
* Use `order: 'asc'` with a `from` bound to drain forward when polling.
|
|
99
|
+
* `from` is inclusive, so a poll that resumes from the last `createdAt` sees
|
|
100
|
+
* that message again; skip ids you have already handled.
|
|
100
101
|
*
|
|
101
102
|
* ```ts
|
|
102
103
|
* for await (const message of textbee.iterateMessages({ direction: 'received' })) {
|
|
@@ -120,7 +121,11 @@ var Textbee = class {
|
|
|
120
121
|
);
|
|
121
122
|
return payload.data;
|
|
122
123
|
}
|
|
123
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Fetch a batch and every message in it, by the `smsBatchId` a send returns.
|
|
126
|
+
* Needs the id of the device that sent the batch. When textbee picked the
|
|
127
|
+
* device, use `getMessages({ smsBatchId })` instead.
|
|
128
|
+
*/
|
|
124
129
|
async getSmsBatch(deviceId, smsBatchId) {
|
|
125
130
|
const payload = await this.#request(
|
|
126
131
|
"GET",
|
|
@@ -156,6 +161,42 @@ var Textbee = class {
|
|
|
156
161
|
return payload;
|
|
157
162
|
}
|
|
158
163
|
};
|
|
164
|
+
function normalizeBaseUrl(raw) {
|
|
165
|
+
const value = raw?.trim();
|
|
166
|
+
if (!value) {
|
|
167
|
+
return DEFAULT_BASE_URL;
|
|
168
|
+
}
|
|
169
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`;
|
|
170
|
+
let url;
|
|
171
|
+
try {
|
|
172
|
+
url = new URL(withScheme);
|
|
173
|
+
} catch {
|
|
174
|
+
throw new TypeError(
|
|
175
|
+
"baseUrl is not a valid URL. Use your textbee api, for example https://sms.example.com. The /api/v1 suffix is added for you."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
179
|
+
throw new TypeError(`baseUrl must be http or https, got "${url.protocol}"`);
|
|
180
|
+
}
|
|
181
|
+
if (url.username || url.password) {
|
|
182
|
+
throw new TypeError("baseUrl must not contain a username or password");
|
|
183
|
+
}
|
|
184
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
185
|
+
let cut = -1;
|
|
186
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
187
|
+
if (segments[i].toLowerCase() === "api" && segments[i + 1].toLowerCase() === "v1") {
|
|
188
|
+
cut = i + 2;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let path;
|
|
193
|
+
if (cut === -1) {
|
|
194
|
+
path = [...segments, "api", "v1"];
|
|
195
|
+
} else {
|
|
196
|
+
path = [...segments.slice(0, cut - 2), "api", "v1"];
|
|
197
|
+
}
|
|
198
|
+
return [url.origin, ...path].join("/");
|
|
199
|
+
}
|
|
159
200
|
function toIsoString(value) {
|
|
160
201
|
return value instanceof Date ? value.toISOString() : value;
|
|
161
202
|
}
|
|
@@ -173,11 +214,15 @@ async function readBody(response) {
|
|
|
173
214
|
function errorMessage(payload, status) {
|
|
174
215
|
if (payload && typeof payload === "object") {
|
|
175
216
|
const record = payload;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
217
|
+
const keys = "statusCode" in record ? ["message", "error"] : ["error", "message"];
|
|
218
|
+
for (const key of keys) {
|
|
219
|
+
const value = record[key];
|
|
220
|
+
if (typeof value === "string" && value) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string")) {
|
|
224
|
+
return value.join("; ");
|
|
225
|
+
}
|
|
181
226
|
}
|
|
182
227
|
}
|
|
183
228
|
return `HTTP ${status}`;
|