@whatsapi.sh/sdk 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/client.d.ts +35 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +169 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/contract.d.ts +318 -0
- package/dist/generated/contract.d.ts.map +1 -0
- package/dist/generated/contract.js +32 -0
- package/dist/generated/contract.js.map +1 -0
- package/dist/generated/operations.d.ts +240 -0
- package/dist/generated/operations.d.ts.map +1 -0
- package/dist/generated/operations.js +285 -0
- package/dist/generated/operations.js.map +1 -0
- package/dist/generated/params.d.ts +177 -0
- package/dist/generated/params.d.ts.map +1 -0
- package/dist/generated/params.js +7 -0
- package/dist/generated/params.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/webhooks.d.ts +28 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +73 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +47 -0
- package/src/client.ts +204 -0
- package/src/errors.ts +71 -0
- package/src/generated/contract.ts +402 -0
- package/src/generated/operations.ts +371 -0
- package/src/generated/params.ts +203 -0
- package/src/index.ts +11 -0
- package/src/webhooks.ts +96 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit. Run `bun run sdk:gen`.
|
|
2
|
+
// Source: apps/api/src/routes/v1 (the mounted operations)
|
|
3
|
+
//
|
|
4
|
+
// One method per capability, named the way its MCP tool is named, returning
|
|
5
|
+
// the DTO the operation declares. `Whatsapi` in ../client.ts supplies request().
|
|
6
|
+
|
|
7
|
+
import type { RequestSpec } from "../client.ts";
|
|
8
|
+
import type {
|
|
9
|
+
BatchMessageDto,
|
|
10
|
+
DeletedNumberDto,
|
|
11
|
+
DeletedWebhookDto,
|
|
12
|
+
MarkReadDto,
|
|
13
|
+
MessageDto,
|
|
14
|
+
NumberCheckDto,
|
|
15
|
+
NumberDto,
|
|
16
|
+
OtpCheckDto,
|
|
17
|
+
OtpDto,
|
|
18
|
+
PairNumberDto,
|
|
19
|
+
SavedWebhookDto,
|
|
20
|
+
TypingDto,
|
|
21
|
+
UnpairedNumberDto,
|
|
22
|
+
UsageDto,
|
|
23
|
+
WebhookDeliveryDto,
|
|
24
|
+
WebhookEndpointDto,
|
|
25
|
+
WebhookTestResultDto,
|
|
26
|
+
} from "./contract.ts";
|
|
27
|
+
import type {
|
|
28
|
+
CheckNumberParams,
|
|
29
|
+
CheckOtpParams,
|
|
30
|
+
CreateNumberParams,
|
|
31
|
+
DeleteNumberParams,
|
|
32
|
+
ListWebhookDeliveriesParams,
|
|
33
|
+
MarkReadParams,
|
|
34
|
+
NumberStatusParams,
|
|
35
|
+
PairNumberParams,
|
|
36
|
+
SendBatchParams,
|
|
37
|
+
SendContactParams,
|
|
38
|
+
SendLocationParams,
|
|
39
|
+
SendMediaParams,
|
|
40
|
+
SendMessageParams,
|
|
41
|
+
SendOtpParams,
|
|
42
|
+
SetWebhookParams,
|
|
43
|
+
TypingParams,
|
|
44
|
+
UnpairNumberParams,
|
|
45
|
+
} from "./params.ts";
|
|
46
|
+
|
|
47
|
+
/** The version this client reports in `x-whatsapi-client`. */
|
|
48
|
+
export const SDK_VERSION = "0.1.0";
|
|
49
|
+
|
|
50
|
+
/** The hosted API. */
|
|
51
|
+
export const DEFAULT_BASE_URL = "https://api.whatsapi.sh/v1";
|
|
52
|
+
|
|
53
|
+
export abstract class GeneratedOperations {
|
|
54
|
+
protected abstract request<T>(spec: RequestSpec, params?: object): Promise<T>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Send a WhatsApp message
|
|
58
|
+
*
|
|
59
|
+
* Send a text message to a phone number over WhatsApp. Counts as one message against the
|
|
60
|
+
* account's monthly quota. The reply carries WhatsApp's message id — delivery and read
|
|
61
|
+
* receipts arrive later on the account's webhook, not here.
|
|
62
|
+
*
|
|
63
|
+
* `POST /messages`
|
|
64
|
+
*/
|
|
65
|
+
sendMessage(params: SendMessageParams): Promise<MessageDto> {
|
|
66
|
+
return this.request({ method: "POST", path: "/messages" }, params);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Send a photo, video, voice note or file
|
|
71
|
+
*
|
|
72
|
+
* Send media over WhatsApp from a public URL or base64 data. `type` picks the bubble the
|
|
73
|
+
* recipient sees: `image` and `video` support a caption, `document` is a file attachment named
|
|
74
|
+
* by `fileName`, and `audio` becomes a voice note when the file is ogg/opus — any other audio
|
|
75
|
+
* format is sent as a playable attachment instead, because WhatsApp will not render a voice
|
|
76
|
+
* note that is not opus. Files are capped at 16MB, and `url` beats `base64` for anything
|
|
77
|
+
* sizeable. Counts as one message.
|
|
78
|
+
*
|
|
79
|
+
* `POST /messages/media`
|
|
80
|
+
*/
|
|
81
|
+
sendMedia(params: SendMediaParams): Promise<MessageDto> {
|
|
82
|
+
return this.request({ method: "POST", path: "/messages/media" }, params);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Send one text to many recipients
|
|
87
|
+
*
|
|
88
|
+
* Send the same text to up to 25 recipients in one call — one message, and one message's worth
|
|
89
|
+
* of quota, per recipient. The answer reports each recipient separately: a batch where some
|
|
90
|
+
* land and some do not is the normal case, not an error. If something systemic stops the run
|
|
91
|
+
* (the number drops its session, the quota or rate limit refuses) the remaining recipients
|
|
92
|
+
* come back as `skipped` rather than being attempted, so they can be sent again once that is
|
|
93
|
+
* cleared. Sends inside a batch leave back-to-back, so keep lists small and space calls
|
|
94
|
+
* minutes apart — pacing keeps your number off WhatsApp's spam radar (see the avoiding-bans
|
|
95
|
+
* guide). Paid plans only.
|
|
96
|
+
*
|
|
97
|
+
* `POST /messages/batch`
|
|
98
|
+
*/
|
|
99
|
+
sendBatch(params: SendBatchParams): Promise<BatchMessageDto> {
|
|
100
|
+
return this.request({ method: "POST", path: "/messages/batch" }, params);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Send a location pin
|
|
105
|
+
*
|
|
106
|
+
* Send a map pin over WhatsApp. Counts as one message.
|
|
107
|
+
*
|
|
108
|
+
* `POST /messages/location`
|
|
109
|
+
*/
|
|
110
|
+
sendLocation(params: SendLocationParams): Promise<MessageDto> {
|
|
111
|
+
return this.request({ method: "POST", path: "/messages/location" }, params);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Send a contact card
|
|
116
|
+
*
|
|
117
|
+
* Send a contact card (vCard) over WhatsApp, so the recipient can save the number with one
|
|
118
|
+
* tap. Counts as one message.
|
|
119
|
+
*
|
|
120
|
+
* `POST /messages/contact`
|
|
121
|
+
*/
|
|
122
|
+
sendContact(params: SendContactParams): Promise<MessageDto> {
|
|
123
|
+
return this.request({ method: "POST", path: "/messages/contact" }, params);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Send a verification code
|
|
128
|
+
*
|
|
129
|
+
* Generate a 6-digit code, send it over WhatsApp and return it. Returning the code is
|
|
130
|
+
* deliberate: verify it yourself, or hand it back to check_otp and keep nothing. Codes expire
|
|
131
|
+
* in 5 minutes by default and one number can only be sent a new code once a minute. Counts as
|
|
132
|
+
* one message.
|
|
133
|
+
*
|
|
134
|
+
* `POST /otp`
|
|
135
|
+
*/
|
|
136
|
+
sendOtp(params: SendOtpParams): Promise<OtpDto> {
|
|
137
|
+
return this.request({ method: "POST", path: "/otp" }, params);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check a verification code
|
|
142
|
+
*
|
|
143
|
+
* Verify a code sent with send_otp and consume it, so it can never be used twice. A wrong or
|
|
144
|
+
* expired code answers 400 OTP_INVALID — deliberately an error, not a `verified: false`, so a
|
|
145
|
+
* missing check at the caller can never read as success. Five wrong guesses kill the code.
|
|
146
|
+
* Free.
|
|
147
|
+
*
|
|
148
|
+
* `POST /otp/check`
|
|
149
|
+
*/
|
|
150
|
+
checkOtp(params: CheckOtpParams): Promise<OtpCheckDto> {
|
|
151
|
+
return this.request({ method: "POST", path: "/otp/check" }, params);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* List your WhatsApp numbers
|
|
156
|
+
*
|
|
157
|
+
* Every WhatsApp number on the account with its connection status. Start here when you do not
|
|
158
|
+
* know which number to send from, or to check whether one needs re-pairing. Free.
|
|
159
|
+
*
|
|
160
|
+
* `GET /numbers`
|
|
161
|
+
*/
|
|
162
|
+
listNumbers(): Promise<NumberDto[]> {
|
|
163
|
+
return this.request({ method: "GET", path: "/numbers" });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add a WhatsApp number
|
|
168
|
+
*
|
|
169
|
+
* Create a new WhatsApp number slot on the account and return it. This is step one of two: the
|
|
170
|
+
* slot starts `disconnected` and cannot send until pair_number links a real WhatsApp account
|
|
171
|
+
* to it. How many can exist at once is what the plan sells. Free.
|
|
172
|
+
*
|
|
173
|
+
* `POST /numbers`
|
|
174
|
+
*/
|
|
175
|
+
createNumber(params: CreateNumberParams): Promise<NumberDto> {
|
|
176
|
+
return this.request({ method: "POST", path: "/numbers" }, params);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check whether a phone is on WhatsApp
|
|
181
|
+
*
|
|
182
|
+
* Ask WhatsApp whether a phone number has an account, and get its profile picture when it
|
|
183
|
+
* does. Brazilian mobiles are checked with and without the extra 9 and the answer reports the
|
|
184
|
+
* form that actually matched. If WhatsApp cannot be reached the answer assumes yes and sets
|
|
185
|
+
* `verified: false` — an outage must not block sends to real people. Free.
|
|
186
|
+
*
|
|
187
|
+
* `GET /numbers/check/:phone`
|
|
188
|
+
*/
|
|
189
|
+
checkNumber(params: CheckNumberParams): Promise<NumberCheckDto> {
|
|
190
|
+
return this.request(
|
|
191
|
+
{ method: "GET", path: "/numbers/check/:phone", pathParams: ["phone"] },
|
|
192
|
+
params,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get one number's status
|
|
198
|
+
*
|
|
199
|
+
* One number's current state: `connected` (ready to send), `pairing` (waiting for a scan),
|
|
200
|
+
* `disconnected` (session dropped — pair it again) or `banned` (WhatsApp restricted it; wait
|
|
201
|
+
* out the 24h window before reconnecting). Free.
|
|
202
|
+
*
|
|
203
|
+
* `GET /numbers/:id`
|
|
204
|
+
*/
|
|
205
|
+
numberStatus(params: NumberStatusParams): Promise<NumberDto> {
|
|
206
|
+
return this.request(
|
|
207
|
+
{ method: "GET", path: "/numbers/:numberId", pathParams: ["numberId"] },
|
|
208
|
+
params,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Pair a number with WhatsApp
|
|
214
|
+
*
|
|
215
|
+
* Start (or resume) linking a WhatsApp account to one of your numbers. `qr` returns a QR code
|
|
216
|
+
* to scan from WhatsApp → Linked devices; `code` returns an 8-character linking code to type
|
|
217
|
+
* there instead, and needs the phone number being linked. A QR lives ~90 seconds and a new one
|
|
218
|
+
* can only be minted every few minutes, so call again to poll: `state: "pairing"` with no code
|
|
219
|
+
* yet simply means it is still being minted. Free.
|
|
220
|
+
*
|
|
221
|
+
* `POST /numbers/:id/pair`
|
|
222
|
+
*/
|
|
223
|
+
pairNumber(params: PairNumberParams): Promise<PairNumberDto> {
|
|
224
|
+
return this.request(
|
|
225
|
+
{ method: "POST", path: "/numbers/:numberId/pair", pathParams: ["numberId"] },
|
|
226
|
+
params,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Disconnect a number from WhatsApp
|
|
232
|
+
*
|
|
233
|
+
* Log the linked WhatsApp account out of this number, keeping the number itself. Use it to
|
|
234
|
+
* link a different phone: a connected number cannot be re-paired until it is unpaired. Free.
|
|
235
|
+
*
|
|
236
|
+
* `POST /numbers/:id/unpair`
|
|
237
|
+
*/
|
|
238
|
+
unpairNumber(params: UnpairNumberParams): Promise<UnpairedNumberDto> {
|
|
239
|
+
return this.request(
|
|
240
|
+
{ method: "POST", path: "/numbers/:numberId/unpair", pathParams: ["numberId"] },
|
|
241
|
+
params,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Delete a number
|
|
247
|
+
*
|
|
248
|
+
* Remove a number from the account for good, logging its WhatsApp session out on the way. This
|
|
249
|
+
* frees a slot against the plan's limit. Messages already sent are unaffected, and the usage
|
|
250
|
+
* they cost stays on the bill. Free, and not reversible.
|
|
251
|
+
*
|
|
252
|
+
* `DELETE /numbers/:id`
|
|
253
|
+
*/
|
|
254
|
+
deleteNumber(params: DeleteNumberParams): Promise<DeletedNumberDto> {
|
|
255
|
+
return this.request(
|
|
256
|
+
{ method: "DELETE", path: "/numbers/:numberId", pathParams: ["numberId"] },
|
|
257
|
+
params,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Show the typing indicator
|
|
263
|
+
*
|
|
264
|
+
* Show (or clear) the “typing…” indicator in a chat. Free — it is not a message. WhatsApp
|
|
265
|
+
* clears it on its own after a few seconds, so call it again for a long reply.
|
|
266
|
+
*
|
|
267
|
+
* `POST /chats/:phone/typing`
|
|
268
|
+
*/
|
|
269
|
+
typing(params: TypingParams): Promise<TypingDto> {
|
|
270
|
+
return this.request(
|
|
271
|
+
{ method: "POST", path: "/chats/:to/typing", pathParams: ["to"] },
|
|
272
|
+
params,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Mark messages as read
|
|
278
|
+
*
|
|
279
|
+
* Put blue ticks on messages your number received in a chat. Free. Message ids come from the
|
|
280
|
+
* inbound webhook.
|
|
281
|
+
*
|
|
282
|
+
* `POST /chats/:phone/read`
|
|
283
|
+
*/
|
|
284
|
+
markRead(params: MarkReadParams): Promise<MarkReadDto> {
|
|
285
|
+
return this.request(
|
|
286
|
+
{ method: "POST", path: "/chats/:to/read", pathParams: ["to"] },
|
|
287
|
+
params,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get your webhook endpoint
|
|
293
|
+
*
|
|
294
|
+
* The endpoint events are being POSTed to, which ones it is subscribed to, and whether it is
|
|
295
|
+
* still healthy. `null` when none is registered. `disabledReason: "failures"` means we stopped
|
|
296
|
+
* delivering after a long run of dead attempts — fix the endpoint and set it again (or fire a
|
|
297
|
+
* test) to switch it back on. Free.
|
|
298
|
+
*
|
|
299
|
+
* `GET /webhooks`
|
|
300
|
+
*/
|
|
301
|
+
getWebhook(): Promise<WebhookEndpointDto | null> {
|
|
302
|
+
return this.request({ method: "GET", path: "/webhooks" });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Register your webhook endpoint
|
|
307
|
+
*
|
|
308
|
+
* Point whatsapi.sh at an https URL and start receiving events there. The answer carries the
|
|
309
|
+
* signing secret ONCE, on first registration: keep it — every delivery is signed with it in
|
|
310
|
+
* the `x-whatsapi-signature` header (`t=<unix>,v1=<hmac-sha256 of "<t>.<body>">`), and that
|
|
311
|
+
* signature is how you know a request is really from us. Calling this again updates the URL or
|
|
312
|
+
* the subscription and keeps the same secret unless you ask to rotate it. Saving also
|
|
313
|
+
* re-enables an endpoint we had disabled. Free.
|
|
314
|
+
*
|
|
315
|
+
* `PUT /webhooks`
|
|
316
|
+
*/
|
|
317
|
+
setWebhook(params: SetWebhookParams): Promise<SavedWebhookDto> {
|
|
318
|
+
return this.request({ method: "PUT", path: "/webhooks" }, params);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Fire a test event at your endpoint
|
|
323
|
+
*
|
|
324
|
+
* Send a `ping` event to your registered endpoint right now and wait for the answer, so you
|
|
325
|
+
* can verify the URL and your signature check without waiting for a real message. The result
|
|
326
|
+
* is the actual outcome: `ok: false` carries the HTTP status your server answered, or why we
|
|
327
|
+
* could not reach it. A successful ping also revives an endpoint we had auto-disabled. Free.
|
|
328
|
+
*
|
|
329
|
+
* `POST /webhooks/test`
|
|
330
|
+
*/
|
|
331
|
+
testWebhook(): Promise<WebhookTestResultDto> {
|
|
332
|
+
return this.request({ method: "POST", path: "/webhooks/test" });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Recent webhook deliveries
|
|
337
|
+
*
|
|
338
|
+
* The last deliveries we attempted, newest first — what we sent, what your endpoint answered,
|
|
339
|
+
* and how many attempts it took. This is the answer to “the event never arrived”. Deliveries
|
|
340
|
+
* are kept for a week. Free.
|
|
341
|
+
*
|
|
342
|
+
* `GET /webhooks/deliveries`
|
|
343
|
+
*/
|
|
344
|
+
listWebhookDeliveries(params?: ListWebhookDeliveriesParams): Promise<WebhookDeliveryDto[]> {
|
|
345
|
+
return this.request({ method: "GET", path: "/webhooks/deliveries" }, params);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Remove your webhook endpoint
|
|
350
|
+
*
|
|
351
|
+
* Stop sending events and forget the endpoint, its secret and its delivery history.
|
|
352
|
+
* Registering again mints a new secret. Free.
|
|
353
|
+
*
|
|
354
|
+
* `DELETE /webhooks`
|
|
355
|
+
*/
|
|
356
|
+
deleteWebhook(): Promise<DeletedWebhookDto> {
|
|
357
|
+
return this.request({ method: "DELETE", path: "/webhooks" });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Check quota and usage
|
|
362
|
+
*
|
|
363
|
+
* Messages sent this month against the plan quota, today against the daily fair-use cap, the
|
|
364
|
+
* leftover credit balance, and the per-key breakdown. Read this before a bulk send. Free.
|
|
365
|
+
*
|
|
366
|
+
* `GET /usage`
|
|
367
|
+
*/
|
|
368
|
+
getUsage(): Promise<UsageDto> {
|
|
369
|
+
return this.request({ method: "GET", path: "/usage" });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit. Run `bun run sdk:gen`.
|
|
2
|
+
// Source: apps/api/src/operations/*.ts (zod input schemas)
|
|
3
|
+
//
|
|
4
|
+
// What each call takes. These are the same schemas the API validates with,
|
|
5
|
+
// so a value TypeScript accepts here is a value the server accepts too.
|
|
6
|
+
|
|
7
|
+
/** Arguments for `sendMessage()`. */
|
|
8
|
+
export interface SendMessageParams {
|
|
9
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
10
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
11
|
+
to: string;
|
|
12
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
13
|
+
* single connected number it is resolved for you. */
|
|
14
|
+
from?: string;
|
|
15
|
+
/** The message body. */
|
|
16
|
+
text: string;
|
|
17
|
+
/** Render a preview card for the first link in the text. Default: on. */
|
|
18
|
+
linkPreview?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Arguments for `sendMedia()`. */
|
|
22
|
+
export interface SendMediaParams {
|
|
23
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
24
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
25
|
+
to: string;
|
|
26
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
27
|
+
* single connected number it is resolved for you. */
|
|
28
|
+
from?: string;
|
|
29
|
+
/** Which kind of media bubble to send. */
|
|
30
|
+
type: "image" | "video" | "audio" | "document";
|
|
31
|
+
/** Public https URL of the file — the better option. We stream the download and forward the
|
|
32
|
+
* bytes, so the size of the file never rides in your request. */
|
|
33
|
+
url?: string;
|
|
34
|
+
/** The file as base64, bare or as a `data:<mime>;base64,…` URI. Base64 inflates a request by a
|
|
35
|
+
* third, so prefer `url` for anything but small files. */
|
|
36
|
+
base64?: string;
|
|
37
|
+
/** Overrides the detected content type, e.g. `image/webp`. */
|
|
38
|
+
mimeType?: string;
|
|
39
|
+
/** Text shown under the media. Images and videos only — WhatsApp has no caption field for
|
|
40
|
+
* documents or audio. */
|
|
41
|
+
caption?: string;
|
|
42
|
+
/** Documents: the filename WhatsApp shows on the bubble. */
|
|
43
|
+
fileName?: string;
|
|
44
|
+
/** Voice notes: duration, so the bubble shows the right length. */
|
|
45
|
+
seconds?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Arguments for `sendBatch()`. */
|
|
49
|
+
export interface SendBatchParams {
|
|
50
|
+
/** Recipients, up to 25. Each gets its own message; no duplicates. */
|
|
51
|
+
to: string[];
|
|
52
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
53
|
+
* single connected number it is resolved for you. */
|
|
54
|
+
from?: string;
|
|
55
|
+
/** The message body — the same for everyone. */
|
|
56
|
+
text: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Arguments for `sendLocation()`. */
|
|
60
|
+
export interface SendLocationParams {
|
|
61
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
62
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
63
|
+
to: string;
|
|
64
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
65
|
+
* single connected number it is resolved for you. */
|
|
66
|
+
from?: string;
|
|
67
|
+
latitude: number;
|
|
68
|
+
longitude: number;
|
|
69
|
+
/** Label shown on the pin. */
|
|
70
|
+
name?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Arguments for `sendContact()`. */
|
|
74
|
+
export interface SendContactParams {
|
|
75
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
76
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
77
|
+
to: string;
|
|
78
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
79
|
+
* single connected number it is resolved for you. */
|
|
80
|
+
from?: string;
|
|
81
|
+
/** Display name on the card. */
|
|
82
|
+
name: string;
|
|
83
|
+
/** The vCard payload, e.g. `BEGIN:VCARD\nVERSION:3.0\n…\nEND:VCARD`. */
|
|
84
|
+
vcard: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Arguments for `sendOtp()`. */
|
|
88
|
+
export interface SendOtpParams {
|
|
89
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
90
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
91
|
+
to: string;
|
|
92
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
93
|
+
* single connected number it is resolved for you. */
|
|
94
|
+
from?: string;
|
|
95
|
+
/** Message body. Must contain `{code}`; `{minutes}` expands to the expiry. Default: “{code} is
|
|
96
|
+
* your verification code. It expires in {minutes} minutes.” */
|
|
97
|
+
template?: string;
|
|
98
|
+
/** How long the code stays valid. Default 300 (5 minutes). */
|
|
99
|
+
expiresInSeconds?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Arguments for `checkOtp()`. */
|
|
103
|
+
export interface CheckOtpParams {
|
|
104
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
105
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
106
|
+
to: string;
|
|
107
|
+
/** The code the user typed. */
|
|
108
|
+
code: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Arguments for `createNumber()`. */
|
|
112
|
+
export interface CreateNumberParams {
|
|
113
|
+
/** A label you choose, e.g. “Support” — only you ever see it. */
|
|
114
|
+
name: string;
|
|
115
|
+
/** Pull the phone's existing chat history on pairing. Off by default: it copies conversations
|
|
116
|
+
* we otherwise never see. */
|
|
117
|
+
historySync?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Arguments for `checkNumber()`. */
|
|
121
|
+
export interface CheckNumberParams {
|
|
122
|
+
/** The phone number to look up, e.g. +5511988887777. */
|
|
123
|
+
phone: string;
|
|
124
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
125
|
+
* single connected number it is resolved for you. */
|
|
126
|
+
from?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Arguments for `numberStatus()`. */
|
|
130
|
+
export interface NumberStatusParams {
|
|
131
|
+
/** Id of the number, from list_numbers. */
|
|
132
|
+
numberId: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Arguments for `pairNumber()`. */
|
|
136
|
+
export interface PairNumberParams {
|
|
137
|
+
/** Id of the number to pair, from list_numbers. */
|
|
138
|
+
numberId: string;
|
|
139
|
+
/** `qr` (default) or `code` for the type-in linking code. */
|
|
140
|
+
mode?: "qr" | "code";
|
|
141
|
+
/** Required with `mode: "code"` — the phone number being linked. */
|
|
142
|
+
phone?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Arguments for `unpairNumber()`. */
|
|
146
|
+
export interface UnpairNumberParams {
|
|
147
|
+
/** Id of the number, from list_numbers. */
|
|
148
|
+
numberId: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Arguments for `deleteNumber()`. */
|
|
152
|
+
export interface DeleteNumberParams {
|
|
153
|
+
/** Id of the number to delete, from list_numbers. */
|
|
154
|
+
numberId: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Arguments for `typing()`. */
|
|
158
|
+
export interface TypingParams {
|
|
159
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
160
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
161
|
+
to: string;
|
|
162
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
163
|
+
* single connected number it is resolved for you. */
|
|
164
|
+
from?: string;
|
|
165
|
+
/** `composing` shows the indicator, `paused` clears it. Default: composing. */
|
|
166
|
+
state?: "composing" | "paused";
|
|
167
|
+
/** Show “recording audio…” instead of “typing…”. */
|
|
168
|
+
recording?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Arguments for `markRead()`. */
|
|
172
|
+
export interface MarkReadParams {
|
|
173
|
+
/** Recipient — a phone number in international format (+5511988887777) or a WhatsApp address
|
|
174
|
+
* (5511988887777@s.whatsapp.net, an @lid contact, or a @g.us group). */
|
|
175
|
+
to: string;
|
|
176
|
+
/** Which of your WhatsApp numbers sends this — its id or its phone number. Optional: with a
|
|
177
|
+
* single connected number it is resolved for you. */
|
|
178
|
+
from?: string;
|
|
179
|
+
/** Ids of the received messages to acknowledge. */
|
|
180
|
+
messageIds: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Arguments for `setWebhook()`. */
|
|
184
|
+
export interface SetWebhookParams {
|
|
185
|
+
/** Where to POST events, e.g. https://api.yourapp.com/webhooks/whatsapp. */
|
|
186
|
+
url: string;
|
|
187
|
+
/** Which events to receive: message.created, message.status, number.connected,
|
|
188
|
+
* number.disconnected, number.logged_out. Defaults to all of them. */
|
|
189
|
+
events?:
|
|
190
|
+
| "message.created"
|
|
191
|
+
| "message.status"
|
|
192
|
+
| "number.connected"
|
|
193
|
+
| "number.disconnected"
|
|
194
|
+
| "number.logged_out"[];
|
|
195
|
+
/** Mint a new signing secret and return it — the old one stops working. */
|
|
196
|
+
rotateSecret?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Arguments for `listWebhookDeliveries()`. */
|
|
200
|
+
export interface ListWebhookDeliveriesParams {
|
|
201
|
+
/** How many to return, newest first. Up to 50 (the default). */
|
|
202
|
+
limit?: number;
|
|
203
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The published surface of `whatsapi`.
|
|
2
|
+
export { Whatsapi } from "./client.ts";
|
|
3
|
+
export type { RequestSpec, WhatsapiOptions } from "./client.ts";
|
|
4
|
+
export { WebhookSignatureError, WhatsapiError } from "./errors.ts";
|
|
5
|
+
export type { ClientErrorCode, WhatsapiErrorInit } from "./errors.ts";
|
|
6
|
+
export { verifyWebhook } from "./webhooks.ts";
|
|
7
|
+
export type { VerifyWebhookInput } from "./webhooks.ts";
|
|
8
|
+
export { DEFAULT_BASE_URL, SDK_VERSION } from "./generated/operations.ts";
|
|
9
|
+
// Every DTO, error code and event shape the API speaks.
|
|
10
|
+
export * from "./generated/contract.ts";
|
|
11
|
+
export type * from "./generated/params.ts";
|
package/src/webhooks.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Receiving events: check the signature, then hand back a typed event.
|
|
2
|
+
//
|
|
3
|
+
// Verifying is not optional. The URL is public, so anything can POST to it —
|
|
4
|
+
// the `x-whatsapi-signature` header is the only thing that separates our
|
|
5
|
+
// delivery from someone else's forgery, and it signs the TIMESTAMP alongside
|
|
6
|
+
// the body so a captured payload cannot be replayed at you later.
|
|
7
|
+
import { WebhookSignatureError } from "./errors.ts";
|
|
8
|
+
import type { WebhookEvent } from "./generated/contract.ts";
|
|
9
|
+
|
|
10
|
+
export interface VerifyWebhookInput {
|
|
11
|
+
/** The RAW request body — the exact bytes we signed. A body that has been
|
|
12
|
+
* parsed and re-serialized will not match, even when it looks identical. */
|
|
13
|
+
payload: string | Uint8Array;
|
|
14
|
+
/** The `x-whatsapi-signature` header. */
|
|
15
|
+
signature: string | null | undefined;
|
|
16
|
+
/** The `whsec_` secret, shown once when the endpoint was registered. */
|
|
17
|
+
secret: string;
|
|
18
|
+
/** How old a delivery may be, in seconds. Default 300. */
|
|
19
|
+
toleranceSeconds?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
|
|
24
|
+
function hex(buffer: ArrayBuffer): string {
|
|
25
|
+
return Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Constant time in the length that matters: comparing hex digit by digit with
|
|
29
|
+
* `===` leaks how much of a guess was right through timing. */
|
|
30
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
31
|
+
if (a.length !== b.length) return false;
|
|
32
|
+
let diff = 0;
|
|
33
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
34
|
+
return diff === 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `t=<unix seconds>,v1=<hex>` — tolerant of spacing and of future `vN=` parts. */
|
|
38
|
+
function parseHeader(header: string): { timestamp: number; v1: string } | null {
|
|
39
|
+
let timestamp = NaN;
|
|
40
|
+
let v1 = "";
|
|
41
|
+
for (const part of header.split(",")) {
|
|
42
|
+
const [key, value = ""] = part.trim().split("=", 2);
|
|
43
|
+
if (key === "t") timestamp = Number(value);
|
|
44
|
+
if (key === "v1") v1 = value;
|
|
45
|
+
}
|
|
46
|
+
return Number.isFinite(timestamp) && v1 ? { timestamp, v1 } : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify a delivery and parse it.
|
|
51
|
+
*
|
|
52
|
+
* Throws `WebhookSignatureError` for anything that does not check out — answer
|
|
53
|
+
* 400 and log it. On success you get the event, discriminated on `type`:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* const event = await verifyWebhook({ payload: rawBody, signature: req.headers["x-whatsapi-signature"], secret });
|
|
57
|
+
* if (event.type === "message.created") reply(event.data.message.text);
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Deliveries retry, so the same `event.id` (also the `x-whatsapi-delivery`
|
|
61
|
+
* header) can arrive twice: record it and drop repeats.
|
|
62
|
+
*/
|
|
63
|
+
export async function verifyWebhook(input: VerifyWebhookInput): Promise<WebhookEvent> {
|
|
64
|
+
const { payload, signature, secret, toleranceSeconds = 300 } = input;
|
|
65
|
+
if (!signature) throw new WebhookSignatureError("Missing x-whatsapi-signature header.");
|
|
66
|
+
if (!secret) throw new WebhookSignatureError("Missing webhook secret.");
|
|
67
|
+
|
|
68
|
+
const parsed = parseHeader(signature);
|
|
69
|
+
if (!parsed) throw new WebhookSignatureError("Malformed x-whatsapi-signature header.");
|
|
70
|
+
|
|
71
|
+
const age = Math.abs(Date.now() / 1000 - parsed.timestamp);
|
|
72
|
+
if (age > toleranceSeconds)
|
|
73
|
+
throw new WebhookSignatureError(
|
|
74
|
+
`Delivery is ${Math.round(age)}s old, outside the ${toleranceSeconds}s tolerance.`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const body = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
78
|
+
const key = await crypto.subtle.importKey(
|
|
79
|
+
"raw",
|
|
80
|
+
encoder.encode(secret),
|
|
81
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
82
|
+
false,
|
|
83
|
+
["sign"],
|
|
84
|
+
);
|
|
85
|
+
const expected = hex(
|
|
86
|
+
await crypto.subtle.sign("HMAC", key, encoder.encode(`${parsed.timestamp}.${body}`)),
|
|
87
|
+
);
|
|
88
|
+
if (!timingSafeEqual(expected, parsed.v1))
|
|
89
|
+
throw new WebhookSignatureError("Signature does not match — the body is not from us.");
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(body) as WebhookEvent;
|
|
93
|
+
} catch {
|
|
94
|
+
throw new WebhookSignatureError("Delivery body is not JSON.");
|
|
95
|
+
}
|
|
96
|
+
}
|