@dojocoding/whatsapp-sdk 0.8.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.
@@ -0,0 +1,1262 @@
1
+ import { S as Storage } from './index-CDfzGvQJ.cjs';
2
+ export { I as InMemoryStorage } from './index-CDfzGvQJ.cjs';
3
+ import { W as WebhookReceiver, a as WhatsAppEvent } from './receiver-DWJm571Z.cjs';
4
+ export { A as AccountAlertEvent, b as AccountReviewEvent, B as BaseEvent, D as DeliveryStatus, E as ErrorHandler, c as EventKindMap, H as HandlePayloadResult, d as Handler, I as IncomingMessageKind, M as MessageEvent, P as PhoneNumberQualityUpdateEvent, S as StatusEvent, T as TemplateCategoryUpdateEvent, e as TemplateQualityUpdateEvent, f as TemplateStatusEvent, U as UnknownEvent, V as VerifyRequestInput, g as VerifyRequestResult, h as WebhookReceiverOptions } from './receiver-DWJm571Z.cjs';
5
+ import { Tracer, Attributes } from '@opentelemetry/api';
6
+
7
+ type TemplateCategory = "MARKETING" | "UTILITY" | "AUTHENTICATION" | (string & {});
8
+ type TemplateStatus = "APPROVED" | "PENDING" | "REJECTED" | "PAUSED" | "DISABLED" | "FLAGGED" | (string & {});
9
+ type TemplateComponentDefinitionType = "HEADER" | "BODY" | "FOOTER" | "BUTTONS" | (string & {});
10
+ interface TemplateButtonDefinition {
11
+ type: "QUICK_REPLY" | "URL" | "PHONE_NUMBER" | "COPY_CODE" | "OTP" | (string & {});
12
+ text?: string;
13
+ url?: string;
14
+ phone_number?: string;
15
+ example?: ReadonlyArray<string>;
16
+ }
17
+ interface TemplateComponentDefinition {
18
+ type: TemplateComponentDefinitionType;
19
+ /** Header format ("TEXT" | "IMAGE" | "VIDEO" | "DOCUMENT" | "LOCATION"). */
20
+ format?: string;
21
+ /** The body / header text containing `{{1}}`, `{{2}}`, … placeholders. */
22
+ text?: string;
23
+ /** Example values Meta shows in the editor. */
24
+ example?: {
25
+ body_text?: ReadonlyArray<ReadonlyArray<string>>;
26
+ header_text?: ReadonlyArray<string>;
27
+ header_handle?: ReadonlyArray<string>;
28
+ };
29
+ /** Buttons component lists buttons here (capitalised in the API). */
30
+ buttons?: ReadonlyArray<TemplateButtonDefinition>;
31
+ }
32
+ interface TemplateDefinition {
33
+ id: string;
34
+ name: string;
35
+ language: string;
36
+ category: TemplateCategory;
37
+ status: TemplateStatus;
38
+ components: ReadonlyArray<TemplateComponentDefinition>;
39
+ quality_score?: {
40
+ score: string;
41
+ date?: number;
42
+ };
43
+ }
44
+ interface ListTemplatesQuery {
45
+ name?: string;
46
+ language?: string;
47
+ status?: TemplateStatus;
48
+ category?: TemplateCategory;
49
+ limit?: number;
50
+ /** Cursor pagination forward. */
51
+ after?: string;
52
+ /** Cursor pagination backward. */
53
+ before?: string;
54
+ }
55
+ interface ListTemplatesPaging {
56
+ cursors?: {
57
+ after?: string;
58
+ before?: string;
59
+ };
60
+ next?: string;
61
+ previous?: string;
62
+ }
63
+ interface ListTemplatesResponse {
64
+ data: ReadonlyArray<TemplateDefinition>;
65
+ paging?: ListTemplatesPaging;
66
+ }
67
+
68
+ type RecipientType = "individual";
69
+ interface BaseMessage {
70
+ messaging_product: "whatsapp";
71
+ recipient_type: RecipientType;
72
+ to: string;
73
+ context?: {
74
+ message_id: string;
75
+ };
76
+ }
77
+ interface TextBody {
78
+ body: string;
79
+ preview_url?: boolean;
80
+ }
81
+ interface TextMessage extends BaseMessage {
82
+ type: "text";
83
+ text: TextBody;
84
+ }
85
+ interface MediaSource {
86
+ /** Pre-uploaded media id from `POST /{phone-number-id}/media`. */
87
+ id?: string;
88
+ /** Public URL Meta will fetch on send. */
89
+ link?: string;
90
+ /** Image / video / document caption. Not used for audio / sticker. */
91
+ caption?: string;
92
+ /** Document filename hint. Not used for image / video / audio / sticker. */
93
+ filename?: string;
94
+ }
95
+ type MediaKind = "image" | "video" | "audio" | "document" | "sticker";
96
+ interface ImageMessage extends BaseMessage {
97
+ type: "image";
98
+ image: Omit<MediaSource, "filename">;
99
+ }
100
+ interface VideoMessage extends BaseMessage {
101
+ type: "video";
102
+ video: Omit<MediaSource, "filename">;
103
+ }
104
+ interface AudioMessage extends BaseMessage {
105
+ type: "audio";
106
+ /**
107
+ * Audio body. Setting `voice: true` makes the WhatsApp client render
108
+ * the message as a push-to-talk voice note (with transcription
109
+ * support, auto-download, and a "played" status when the recipient
110
+ * listens) instead of a music file. Source:
111
+ * https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/audio-messages
112
+ */
113
+ audio: Pick<MediaSource, "id" | "link"> & {
114
+ voice?: boolean;
115
+ };
116
+ }
117
+ interface DocumentMessage extends BaseMessage {
118
+ type: "document";
119
+ document: MediaSource;
120
+ }
121
+ interface StickerMessage extends BaseMessage {
122
+ type: "sticker";
123
+ sticker: Pick<MediaSource, "id" | "link">;
124
+ }
125
+ interface LocationBody {
126
+ latitude: number;
127
+ longitude: number;
128
+ name?: string;
129
+ address?: string;
130
+ }
131
+ interface LocationMessage extends BaseMessage {
132
+ type: "location";
133
+ location: LocationBody;
134
+ }
135
+ interface ContactName {
136
+ formatted_name: string;
137
+ first_name?: string;
138
+ last_name?: string;
139
+ middle_name?: string;
140
+ prefix?: string;
141
+ suffix?: string;
142
+ }
143
+ interface ContactPhone {
144
+ phone: string;
145
+ type?: "HOME" | "WORK" | "CELL" | "MAIN" | "IPHONE";
146
+ wa_id?: string;
147
+ }
148
+ interface ContactEmail {
149
+ email: string;
150
+ type?: "HOME" | "WORK";
151
+ }
152
+ interface Contact {
153
+ name: ContactName;
154
+ phones?: ReadonlyArray<ContactPhone>;
155
+ emails?: ReadonlyArray<ContactEmail>;
156
+ org?: {
157
+ company?: string;
158
+ department?: string;
159
+ title?: string;
160
+ };
161
+ birthday?: string;
162
+ }
163
+ interface ContactsMessage extends BaseMessage {
164
+ type: "contacts";
165
+ contacts: ReadonlyArray<Contact>;
166
+ }
167
+ interface InteractiveHeaderText {
168
+ type: "text";
169
+ text: string;
170
+ }
171
+ interface InteractiveHeaderImage {
172
+ type: "image";
173
+ image: {
174
+ id?: string;
175
+ link?: string;
176
+ };
177
+ }
178
+ interface InteractiveHeaderVideo {
179
+ type: "video";
180
+ video: {
181
+ id?: string;
182
+ link?: string;
183
+ };
184
+ }
185
+ interface InteractiveHeaderDocument {
186
+ type: "document";
187
+ document: {
188
+ id?: string;
189
+ link?: string;
190
+ filename?: string;
191
+ };
192
+ }
193
+ type InteractiveHeader = InteractiveHeaderText | InteractiveHeaderImage | InteractiveHeaderVideo | InteractiveHeaderDocument;
194
+ interface InteractiveButtonReply {
195
+ type: "reply";
196
+ reply: {
197
+ id: string;
198
+ title: string;
199
+ };
200
+ }
201
+ interface InteractiveButtonAction {
202
+ buttons: ReadonlyArray<InteractiveButtonReply>;
203
+ }
204
+ interface InteractiveListRow {
205
+ id: string;
206
+ title: string;
207
+ description?: string;
208
+ }
209
+ interface InteractiveListSection {
210
+ title: string;
211
+ rows: ReadonlyArray<InteractiveListRow>;
212
+ }
213
+ interface InteractiveListAction {
214
+ button: string;
215
+ sections: ReadonlyArray<InteractiveListSection>;
216
+ }
217
+ interface InteractiveCtaUrlAction {
218
+ name: "cta_url";
219
+ parameters: {
220
+ display_text: string;
221
+ url: string;
222
+ };
223
+ }
224
+ interface InteractiveButtonBody {
225
+ type: "button";
226
+ header?: InteractiveHeader;
227
+ body: {
228
+ text: string;
229
+ };
230
+ footer?: {
231
+ text: string;
232
+ };
233
+ action: InteractiveButtonAction;
234
+ }
235
+ interface InteractiveListBody {
236
+ type: "list";
237
+ header?: InteractiveHeaderText;
238
+ body: {
239
+ text: string;
240
+ };
241
+ footer?: {
242
+ text: string;
243
+ };
244
+ action: InteractiveListAction;
245
+ }
246
+ interface InteractiveCtaUrlBody {
247
+ type: "cta_url";
248
+ header?: InteractiveHeader;
249
+ body: {
250
+ text: string;
251
+ };
252
+ footer?: {
253
+ text: string;
254
+ };
255
+ action: InteractiveCtaUrlAction;
256
+ }
257
+ type InteractiveBody = InteractiveButtonBody | InteractiveListBody | InteractiveCtaUrlBody;
258
+ interface InteractiveMessage extends BaseMessage {
259
+ type: "interactive";
260
+ interactive: InteractiveBody;
261
+ }
262
+ interface TemplateLanguage {
263
+ code: string;
264
+ policy?: "deterministic";
265
+ }
266
+ interface TemplateParameterText {
267
+ type: "text";
268
+ text: string;
269
+ }
270
+ interface TemplateParameterCurrency {
271
+ type: "currency";
272
+ currency: {
273
+ fallback_value: string;
274
+ code: string;
275
+ amount_1000: number;
276
+ };
277
+ }
278
+ interface TemplateParameterDateTime {
279
+ type: "date_time";
280
+ date_time: {
281
+ fallback_value: string;
282
+ };
283
+ }
284
+ interface TemplateParameterImage {
285
+ type: "image";
286
+ image: {
287
+ id?: string;
288
+ link?: string;
289
+ };
290
+ }
291
+ interface TemplateParameterVideo {
292
+ type: "video";
293
+ video: {
294
+ id?: string;
295
+ link?: string;
296
+ };
297
+ }
298
+ interface TemplateParameterDocument {
299
+ type: "document";
300
+ document: {
301
+ id?: string;
302
+ link?: string;
303
+ filename?: string;
304
+ };
305
+ }
306
+ /**
307
+ * Limited-time-offer parameter (paired with a `type: "limited_time_offer"`
308
+ * component). `expiration_time_ms` is Unix epoch milliseconds. Source:
309
+ * https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/limited-time-offer-templates/
310
+ */
311
+ interface TemplateParameterLimitedTimeOffer {
312
+ type: "limited_time_offer";
313
+ limited_time_offer: {
314
+ expiration_time_ms: number;
315
+ };
316
+ }
317
+ /**
318
+ * Coupon-code parameter (paired with a `sub_type: "copy_code"` button).
319
+ * Source: same Meta doc as `TemplateParameterLimitedTimeOffer`.
320
+ */
321
+ interface TemplateParameterCouponCode {
322
+ type: "coupon_code";
323
+ coupon_code: string;
324
+ }
325
+ /**
326
+ * Payload parameter for `sub_type: "quick_reply"` buttons (used inside
327
+ * carousel cards). Source:
328
+ * https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/media-card-carousel-templates/
329
+ */
330
+ interface TemplateParameterPayload {
331
+ type: "payload";
332
+ payload: string;
333
+ }
334
+ type TemplateParameter = TemplateParameterText | TemplateParameterCurrency | TemplateParameterDateTime | TemplateParameterImage | TemplateParameterVideo | TemplateParameterDocument | TemplateParameterLimitedTimeOffer | TemplateParameterCouponCode | TemplateParameterPayload;
335
+ /**
336
+ * A single carousel card. The `card_index` is 0-based and assigned by
337
+ * the builder, never by the caller. Per Meta's docs the card's
338
+ * `components` array contains a required `header` plus optional
339
+ * `body` and `button` sub-components.
340
+ */
341
+ interface CarouselCardComponent {
342
+ card_index: number;
343
+ components: ReadonlyArray<TemplateComponent>;
344
+ }
345
+ interface TemplateComponent {
346
+ type: "header" | "body" | "button" | "footer" | "carousel" | "limited_time_offer";
347
+ sub_type?: "quick_reply" | "url" | "copy_code";
348
+ /**
349
+ * Meta's docs use a string for auth-template buttons (`"0"`) and a
350
+ * number for carousel-card buttons (`0`). Both work at the API; we
351
+ * mirror what Meta publishes so reviewers can diff without mental
352
+ * conversion.
353
+ */
354
+ index?: string | number;
355
+ parameters?: ReadonlyArray<TemplateParameter>;
356
+ /** Only set when `type === "carousel"`. */
357
+ cards?: ReadonlyArray<CarouselCardComponent>;
358
+ }
359
+ interface TemplateBody {
360
+ name: string;
361
+ language: TemplateLanguage;
362
+ components?: ReadonlyArray<TemplateComponent>;
363
+ }
364
+ interface TemplateMessage extends BaseMessage {
365
+ type: "template";
366
+ template: TemplateBody;
367
+ }
368
+ interface ReactionBody {
369
+ message_id: string;
370
+ emoji: string;
371
+ }
372
+ interface ReactionMessage extends BaseMessage {
373
+ type: "reaction";
374
+ reaction: ReactionBody;
375
+ }
376
+ type WhatsAppMessage = TextMessage | ImageMessage | VideoMessage | AudioMessage | DocumentMessage | StickerMessage | LocationMessage | ContactsMessage | InteractiveMessage | TemplateMessage | ReactionMessage;
377
+ interface MessageSendResponseContact {
378
+ input: string;
379
+ wa_id: string;
380
+ }
381
+ interface MessageSendResponseMessage {
382
+ id: string;
383
+ message_status?: string;
384
+ }
385
+ interface MessageSendResponse {
386
+ messaging_product: "whatsapp";
387
+ contacts: ReadonlyArray<MessageSendResponseContact>;
388
+ messages: ReadonlyArray<MessageSendResponseMessage>;
389
+ }
390
+
391
+ interface BuildTextInput {
392
+ to: string;
393
+ body: string;
394
+ previewUrl?: boolean;
395
+ replyTo?: string;
396
+ }
397
+ declare function buildText(input: BuildTextInput): TextMessage;
398
+ interface BuildMediaInput {
399
+ to: string;
400
+ id?: string;
401
+ link?: string;
402
+ caption?: string;
403
+ filename?: string;
404
+ replyTo?: string;
405
+ }
406
+ declare function buildImage(input: BuildMediaInput): ImageMessage;
407
+ declare function buildVideo(input: BuildMediaInput): VideoMessage;
408
+ declare function buildAudio(input: BuildMediaInput): AudioMessage;
409
+ declare function buildDocument(input: BuildMediaInput): DocumentMessage;
410
+ declare function buildSticker(input: BuildMediaInput): StickerMessage;
411
+ interface BuildLocationInput {
412
+ to: string;
413
+ latitude: number;
414
+ longitude: number;
415
+ name?: string;
416
+ address?: string;
417
+ replyTo?: string;
418
+ }
419
+ declare function buildLocation(input: BuildLocationInput): LocationMessage;
420
+ interface BuildContactsInput {
421
+ to: string;
422
+ contacts: Contact | ReadonlyArray<Contact>;
423
+ replyTo?: string;
424
+ }
425
+ declare function buildContacts(input: BuildContactsInput): ContactsMessage;
426
+ interface BuildInteractiveButtonInput {
427
+ to: string;
428
+ header?: InteractiveHeader;
429
+ body: string;
430
+ footer?: string;
431
+ buttons: ReadonlyArray<{
432
+ id: string;
433
+ title: string;
434
+ }>;
435
+ replyTo?: string;
436
+ }
437
+ declare function buildInteractiveButton(input: BuildInteractiveButtonInput): InteractiveMessage;
438
+ interface BuildInteractiveListInput {
439
+ to: string;
440
+ header?: {
441
+ type: "text";
442
+ text: string;
443
+ };
444
+ body: string;
445
+ footer?: string;
446
+ button: string;
447
+ sections: ReadonlyArray<InteractiveListSection>;
448
+ replyTo?: string;
449
+ }
450
+ declare function buildInteractiveList(input: BuildInteractiveListInput): InteractiveMessage;
451
+ interface BuildInteractiveCtaUrlInput {
452
+ to: string;
453
+ header?: InteractiveHeader;
454
+ body: string;
455
+ footer?: string;
456
+ cta: {
457
+ displayText: string;
458
+ url: string;
459
+ };
460
+ replyTo?: string;
461
+ }
462
+ declare function buildInteractiveCtaUrl(input: BuildInteractiveCtaUrlInput): InteractiveMessage;
463
+ type BuildInteractiveInput = ({
464
+ kind: "button";
465
+ } & BuildInteractiveButtonInput) | ({
466
+ kind: "list";
467
+ } & BuildInteractiveListInput) | ({
468
+ kind: "cta_url";
469
+ } & BuildInteractiveCtaUrlInput);
470
+ declare function buildInteractive(input: BuildInteractiveInput): InteractiveMessage;
471
+ interface BuildTemplateInput {
472
+ to: string;
473
+ name: string;
474
+ language: string;
475
+ components?: ReadonlyArray<TemplateComponent>;
476
+ replyTo?: string;
477
+ /**
478
+ * Optional approved-template definition to cross-validate against
479
+ * before building. When supplied, parameter counts and component
480
+ * shapes are checked locally and `TemplateError` is thrown on
481
+ * mismatch, avoiding the wasted HTTP round-trip to Meta.
482
+ */
483
+ validateAgainst?: TemplateDefinition;
484
+ }
485
+ declare function buildTemplate(input: BuildTemplateInput): TemplateMessage;
486
+ interface BuildReactionInput {
487
+ to: string;
488
+ messageId: string;
489
+ /** Empty string clears a previously set reaction. */
490
+ emoji: string;
491
+ replyTo?: string;
492
+ }
493
+ declare function buildReaction(input: BuildReactionInput): ReactionMessage;
494
+ interface BuildAuthTemplateInput {
495
+ to: string;
496
+ /** Template name (case-sensitive, must exist as APPROVED in your WABA). */
497
+ name: string;
498
+ /** BCP-47 language code — must match the approved template's language. */
499
+ language: string;
500
+ /**
501
+ * The one-time password / verification code. Meta caps this at 15
502
+ * characters. Appears in BOTH the body and URL-button parameters of
503
+ * the documented wire payload — the builder duplicates it for you.
504
+ */
505
+ otp: string;
506
+ /**
507
+ * Index of the URL button on the approved template. Defaults to `"0"`
508
+ * (matches Meta's documented example). Override only if your approved
509
+ * template has the OTP button at a non-zero position.
510
+ */
511
+ otpButtonIndex?: string | number;
512
+ replyTo?: string;
513
+ }
514
+ /**
515
+ * Build an authentication-template (OTP) send payload. The wire shape
516
+ * matches Meta's documented copy-code button authentication template
517
+ * payload at
518
+ * https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/copy-code-button-authentication-templates/.
519
+ *
520
+ * The same wire shape applies to one-tap autofill and zero-tap
521
+ * authentication templates — the subtype distinction lives at template
522
+ * creation time (Meta Business Manager / the templates API), not at
523
+ * send time.
524
+ *
525
+ * The OTP code appears in BOTH the body component's `text` parameter
526
+ * AND the URL button component's `text` parameter. This is Meta's
527
+ * documented requirement, not an SDK quirk; the builder duplicates the
528
+ * code so consumers don't have to remember to pass it twice.
529
+ */
530
+ declare function buildAuthTemplate(input: BuildAuthTemplateInput): TemplateMessage;
531
+ interface BuildVoiceInput {
532
+ to: string;
533
+ /** Pre-uploaded media id from `POST /{phone-number-id}/media`. Exactly one of `id` / `link` must be supplied. */
534
+ id?: string;
535
+ /** Public URL Meta will fetch on send. Exactly one of `id` / `link` must be supplied. */
536
+ link?: string;
537
+ replyTo?: string;
538
+ }
539
+ /**
540
+ * Build a voice-note send payload — an audio message with the
541
+ * `voice: true` flag set. Source:
542
+ * https://developers.facebook.com/documentation/business-messaging/whatsapp/messages/audio-messages
543
+ *
544
+ * Setting `voice: true` triggers transcription support, auto-download,
545
+ * and a "played" delivery status when the recipient listens. Without
546
+ * the flag the same media renders as a regular audio file with the
547
+ * music-player UI.
548
+ *
549
+ * The media payload (`.ogg` with OPUS codec is recommended) is supplied
550
+ * the same way as for `buildAudio` — either a pre-uploaded media id or
551
+ * a public link.
552
+ */
553
+ declare function buildVoice(input: BuildVoiceInput): AudioMessage;
554
+ interface CarouselCardMediaHeader {
555
+ type: "image" | "video";
556
+ /** Pre-uploaded media id. Exactly one of `mediaId` / `link` must be supplied. */
557
+ mediaId?: string;
558
+ /** Public link. Exactly one of `mediaId` / `link` must be supplied. */
559
+ link?: string;
560
+ }
561
+ type CarouselCardButton = {
562
+ subType: "quick_reply";
563
+ payload: string;
564
+ } | {
565
+ subType: "url";
566
+ text: string;
567
+ };
568
+ interface CarouselCard {
569
+ /** Image or video header for this card (required by Meta). */
570
+ header: CarouselCardMediaHeader;
571
+ /** Body-text variable substitutions for this card's approved-template body. */
572
+ bodyParameters?: ReadonlyArray<string>;
573
+ /**
574
+ * Up to two buttons per card (Meta cap). Order matters — the index
575
+ * in the wire payload is the position in this array.
576
+ */
577
+ buttons?: ReadonlyArray<CarouselCardButton>;
578
+ }
579
+ interface BuildCarouselTemplateInput {
580
+ to: string;
581
+ name: string;
582
+ language: string;
583
+ /** Top-level body-text variable substitutions for the carousel's leading body component. */
584
+ bodyParameters?: ReadonlyArray<string>;
585
+ cards: ReadonlyArray<CarouselCard>;
586
+ replyTo?: string;
587
+ }
588
+ /**
589
+ * Build a media-card carousel template send payload. The wire shape
590
+ * matches Meta's documented payload at
591
+ * https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/media-card-carousel-templates/.
592
+ *
593
+ * Cards are bounded to 1–10 per Meta's documented maximum. Each card's
594
+ * `card_index` is computed from its position in the input array so
595
+ * consumers cannot misorder it.
596
+ */
597
+ declare function buildCarouselTemplate(input: BuildCarouselTemplateInput): TemplateMessage;
598
+
599
+ declare const GRAPH_API_VERSION: "v25.0";
600
+ declare const META_GRAPH_BASE_URL: "https://graph.facebook.com";
601
+ declare const WEBHOOK_ACK_DEADLINE_MS: 30000;
602
+ declare const WINDOW_TTL_MS: number;
603
+ declare const WEBHOOK_DEDUPE_TTL_MS: number;
604
+ type GraphApiVersion = typeof GRAPH_API_VERSION | `v${number}.${number}`;
605
+
606
+ interface WindowTrackerOptions {
607
+ /** The phone number id this tracker scopes its keys to. */
608
+ phoneNumberId: string;
609
+ /** Async key/value store. Use `InMemoryStorage` for dev, BYO Redis for prod. */
610
+ storage: Storage;
611
+ /** Window TTL in milliseconds. Defaults to {@link WINDOW_TTL_MS} (24h). */
612
+ ttlMs?: number;
613
+ }
614
+ /**
615
+ * 24-hour customer-service-window tracker. Records the most recent inbound
616
+ * timestamp per `customerWaId`, scoped to a single `phoneNumberId`, and
617
+ * exposes `isWindowOpen` for pre-flight checks on outbound free-form
618
+ * sends.
619
+ *
620
+ * Wire it from your inbound handler:
621
+ * receiver.on("message", (e) => tracker.notifyInbound(e.from));
622
+ *
623
+ * And from your outbound client:
624
+ * const client = new WhatsAppClient({ ..., windowTracker: tracker });
625
+ */
626
+ declare class WindowTracker {
627
+ #private;
628
+ readonly phoneNumberId: string;
629
+ constructor(options: WindowTrackerOptions);
630
+ get ttlMs(): number;
631
+ notifyInbound(customerWaId: string, _atMs?: number): Promise<void>;
632
+ isWindowOpen(customerWaId: string): Promise<boolean>;
633
+ /** @internal — exposed so consumers can clear a window after a hard error. */
634
+ clear(customerWaId: string): Promise<void>;
635
+ }
636
+
637
+ interface RetryPolicy {
638
+ /** Total attempts including the first call. */
639
+ maxAttempts: number;
640
+ /** Base delay used as the seed for exponential backoff. */
641
+ baseDelayMs: number;
642
+ /** Hard cap on any single delay. */
643
+ maxDelayMs: number;
644
+ /** Jitter strategy. Only "full" is supported in v1. */
645
+ jitter: "full";
646
+ /** Lower bound on every delay (avoids 0-ms hammering when jitter rolls 0). */
647
+ floorMs: number;
648
+ }
649
+ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
650
+ /**
651
+ * Marker thrown by callers (or the transport layer) to signal a transient
652
+ * HTTP failure that should be retried. Carries optional Retry-After hint
653
+ * derived from the response headers.
654
+ */
655
+ declare class TransientHttpError extends Error {
656
+ readonly retryAfterMs: number | undefined;
657
+ constructor(message: string, retryAfterMs?: number);
658
+ }
659
+ interface RetryHooks {
660
+ /** Optional sleep injection — in tests we pass a `vi.advanceTimersByTime`-friendly stub. */
661
+ sleep?: (ms: number) => Promise<void>;
662
+ /** Optional RNG injection for deterministic jitter testing. Returns a value in [0, 1). */
663
+ random?: () => number;
664
+ }
665
+
666
+ type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
667
+ interface RequestOptions {
668
+ /** Override the default retry policy for this call. */
669
+ retryPolicy?: RetryPolicy;
670
+ /** Test hooks; never set in production. */
671
+ retryHooks?: RetryHooks;
672
+ /** Per-call AbortSignal; cancellation is treated as a retryable error. */
673
+ signal?: AbortSignal;
674
+ /**
675
+ * Override the resolved Graph API version for this call (rare — only
676
+ * useful for cross-version migrations).
677
+ */
678
+ graphApiVersion?: string;
679
+ /**
680
+ * Optional caller-provided idempotency key. When omitted, the SDK
681
+ * generates a fresh UUID v4 per logical call and reuses it across
682
+ * retry attempts of that call. Sent as `X-Dojo-Idempotency-Key`.
683
+ *
684
+ * Meta does NOT honour this header — a retry of `POST /messages`
685
+ * creates a new send. The key is for client-side correlation only
686
+ * (internal logs, parity replays in the mock, future replay-buffering).
687
+ * See `docs/compliance.md` § 3.4.
688
+ */
689
+ idempotencyKey?: string;
690
+ /** Override fetch implementation — internal hook used by tests. */
691
+ fetchImpl?: typeof fetch;
692
+ }
693
+
694
+ interface TokenInfo {
695
+ /** Whether Meta considers the token currently usable. */
696
+ valid: boolean;
697
+ /**
698
+ * Token expiration, expressed as Unix epoch milliseconds. `null` when
699
+ * Meta returns 0 or omits the field (some long-lived tokens never expire).
700
+ */
701
+ expiresAt: number | null;
702
+ appId: string | null;
703
+ userId: string | null;
704
+ scopes: ReadonlyArray<string>;
705
+ }
706
+
707
+ /**
708
+ * Resolves the bearer token used for Graph API requests. Invoked
709
+ * exactly once per outer `request()` call; the resolved value is used
710
+ * for all retry attempts within that request. Throw, return an empty
711
+ * string, or return a non-string value to surface as `AuthenticationError`
712
+ * before the HTTP request is made.
713
+ *
714
+ * Use this shape when tokens rotate (System User expiry, manual
715
+ * rotation in Business Manager, refresh after a 401). The SDK does
716
+ * NOT cache the resolved value across requests; cache inside your
717
+ * callback if needed.
718
+ */
719
+ type TokenProvider = () => string | Promise<string>;
720
+ interface WhatsAppClientOptions {
721
+ /** Phone number ID for the WhatsApp Business phone (Graph API: /{phone-number-id}/messages). */
722
+ phoneNumberId: string;
723
+ /** WhatsApp Business Account ID (Graph API: /{waba-id}/message_templates). */
724
+ wabaId: string;
725
+ /**
726
+ * Long-lived BISU or System User token, or a `TokenProvider` callback
727
+ * that resolves one per request. Callback shape supports rotation
728
+ * without swapping the client instance.
729
+ */
730
+ token: string | TokenProvider;
731
+ /** App secret used to verify HMAC-SHA256 signatures on inbound webhooks. */
732
+ appSecret: string;
733
+ /** Optional override for the pinned Graph API version (default: GRAPH_API_VERSION). */
734
+ graphApiVersion?: GraphApiVersion;
735
+ /**
736
+ * Optional 24h-window tracker. When provided, free-form sends
737
+ * (sendText, sendMedia*, sendLocation, sendContacts, sendInteractive)
738
+ * pre-flight-check the tracker and throw `WindowClosedError` before
739
+ * issuing any HTTP request. `sendTemplate` and `sendReaction` are
740
+ * window-exempt and never consult the tracker.
741
+ */
742
+ windowTracker?: WindowTracker;
743
+ }
744
+ declare class WhatsAppClient {
745
+ #private;
746
+ readonly phoneNumberId: string;
747
+ readonly wabaId: string;
748
+ readonly graphApiVersion: GraphApiVersion;
749
+ constructor(options: WhatsAppClientOptions);
750
+ /**
751
+ * Whether the 24h customer-service window is currently open for `to`.
752
+ * Returns `true` when no window tracker is configured (preserving the
753
+ * pre-Phase-4 "ungated" behaviour); otherwise delegates to the tracker.
754
+ */
755
+ isWindowOpen(to: string): Promise<boolean>;
756
+ /**
757
+ * @internal — exposed for capability slices that need the bearer
758
+ * token. Resolves the configured `TokenProvider` exactly once per
759
+ * call; surfaces provider failures as `AuthenticationError` before
760
+ * the HTTP request is made.
761
+ */
762
+ _resolveBearerToken(): Promise<string>;
763
+ /** @internal — exposed for the webhook receiver capability (Phase 3). */
764
+ _getAppSecret(): string;
765
+ /**
766
+ * Issue an authenticated Graph API request.
767
+ *
768
+ * @internal — public-API surface for sends, templates, etc. lands in
769
+ * later phases (Phase 2 message-builders, Phase 5 template-management).
770
+ */
771
+ request<T>(method: HttpMethod, path: string, body?: unknown, options?: RequestOptions): Promise<T>;
772
+ /**
773
+ * Verify the bearer token via Meta's `/debug_token` endpoint.
774
+ *
775
+ * Resolves with the parsed token info; throws `WhatsAppError` if the
776
+ * token is invalid or the call fails.
777
+ */
778
+ healthCheck(options?: RequestOptions): Promise<TokenInfo>;
779
+ sendText(input: BuildTextInput, options?: RequestOptions): Promise<MessageSendResponse>;
780
+ sendImage(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
781
+ sendVideo(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
782
+ sendAudio(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
783
+ sendDocument(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
784
+ sendSticker(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
785
+ sendLocation(input: BuildLocationInput, options?: RequestOptions): Promise<MessageSendResponse>;
786
+ sendContacts(input: BuildContactsInput, options?: RequestOptions): Promise<MessageSendResponse>;
787
+ sendInteractive(input: BuildInteractiveInput, options?: RequestOptions): Promise<MessageSendResponse>;
788
+ /**
789
+ * Window-exempt: templates are the escape hatch when the window is closed.
790
+ * Async so any synchronous error from `buildTemplate` (e.g.,
791
+ * `validateAgainst` mismatch) surfaces as a rejected promise rather
792
+ * than a synchronous throw.
793
+ */
794
+ sendTemplate(input: BuildTemplateInput, options?: RequestOptions): Promise<MessageSendResponse>;
795
+ /** Window-exempt: authentication templates are the canonical out-of-window send. */
796
+ sendAuthTemplate(input: BuildAuthTemplateInput, options?: RequestOptions): Promise<MessageSendResponse>;
797
+ /**
798
+ * Send a voice note (audio with `voice: true`). Window-gated like
799
+ * any other free-form media send. Voice notes trigger transcription
800
+ * support, auto-download, and a "played" status when the recipient
801
+ * listens.
802
+ */
803
+ sendVoice(input: BuildVoiceInput, options?: RequestOptions): Promise<MessageSendResponse>;
804
+ /** Window-exempt: carousel sends are template sends. */
805
+ sendCarouselTemplate(input: BuildCarouselTemplateInput, options?: RequestOptions): Promise<MessageSendResponse>;
806
+ listTemplates(query?: ListTemplatesQuery, options?: RequestOptions): Promise<ListTemplatesResponse>;
807
+ getTemplate(templateId: string, options?: RequestOptions): Promise<TemplateDefinition>;
808
+ /** Window-exempt: reactions are part of an existing thread. */
809
+ sendReaction(input: BuildReactionInput, options?: RequestOptions): Promise<MessageSendResponse>;
810
+ /**
811
+ * Send any pre-built `WhatsAppMessage` payload as a reply to a previous
812
+ * message identified by its wamid. Sets `context.message_id` and posts.
813
+ * Window-gated for non-template, non-reaction payloads.
814
+ */
815
+ sendReply(replyTo: string, payload: WhatsAppMessage, options?: RequestOptions): Promise<MessageSendResponse>;
816
+ }
817
+
818
+ /**
819
+ * POST a fully-built `WhatsAppMessage` payload to `/{phoneNumberId}/messages`.
820
+ * Returns the parsed response body (`{ messaging_product, contacts, messages }`).
821
+ */
822
+ declare function sendMessage(client: WhatsAppClient, payload: WhatsAppMessage, options?: RequestOptions): Promise<MessageSendResponse>;
823
+
824
+ type WhatsAppErrorCode = "MISSING_CREDENTIALS" | "RATE_LIMIT" | "WINDOW_CLOSED" | "WEBHOOK_SIGNATURE" | "TEMPLATE" | "MOCK_MODE" | "AUTHENTICATION" | "PERMISSION" | "CAPABILITY" | "UNKNOWN";
825
+ interface WhatsAppErrorOptions {
826
+ cause?: unknown;
827
+ }
828
+ declare class WhatsAppError extends Error {
829
+ readonly code: WhatsAppErrorCode;
830
+ constructor(code: WhatsAppErrorCode, message: string, options?: WhatsAppErrorOptions);
831
+ toJSON(): {
832
+ name: string;
833
+ code: WhatsAppErrorCode;
834
+ message: string;
835
+ };
836
+ }
837
+ type CredentialField = "phoneNumberId" | "wabaId" | "token" | "appSecret";
838
+ declare class MissingCredentialsError extends WhatsAppError {
839
+ readonly code: "MISSING_CREDENTIALS";
840
+ readonly missingFields: ReadonlyArray<CredentialField>;
841
+ constructor(missingFields: ReadonlyArray<CredentialField>, options?: WhatsAppErrorOptions);
842
+ toJSON(): {
843
+ name: string;
844
+ code: "MISSING_CREDENTIALS";
845
+ message: string;
846
+ missingFields: ReadonlyArray<CredentialField>;
847
+ };
848
+ }
849
+ interface RateLimitErrorMeta {
850
+ /** Meta error code (e.g., 131056, 130429, 131048). */
851
+ metaCode?: number;
852
+ /** Retry hint in milliseconds, derived from headers or backoff. */
853
+ retryAfterMs?: number;
854
+ }
855
+ declare class RateLimitError extends WhatsAppError {
856
+ readonly code: "RATE_LIMIT";
857
+ readonly metaCode: number | undefined;
858
+ readonly retryAfterMs: number | undefined;
859
+ constructor(message: string, meta?: RateLimitErrorMeta, options?: WhatsAppErrorOptions);
860
+ }
861
+ declare class WindowClosedError extends WhatsAppError {
862
+ readonly code: "WINDOW_CLOSED";
863
+ readonly customerWaId: string;
864
+ constructor(customerWaId: string, options?: WhatsAppErrorOptions);
865
+ }
866
+ declare class WebhookSignatureError extends WhatsAppError {
867
+ readonly code: "WEBHOOK_SIGNATURE";
868
+ constructor(message?: string, options?: WhatsAppErrorOptions);
869
+ }
870
+ declare class TemplateError extends WhatsAppError {
871
+ readonly code: "TEMPLATE";
872
+ readonly templateName: string | undefined;
873
+ constructor(message: string, templateName?: string, options?: WhatsAppErrorOptions);
874
+ }
875
+ declare class MockModeError extends WhatsAppError {
876
+ readonly code: "MOCK_MODE";
877
+ constructor(message: string, options?: WhatsAppErrorOptions);
878
+ }
879
+ interface AuthenticationErrorMeta {
880
+ /** Meta error code (typically 190). */
881
+ metaCode?: number;
882
+ /** Meta error_subcode (e.g. 463 expired, 467 invalid, 492 changed). */
883
+ subcode?: number;
884
+ }
885
+ declare class AuthenticationError extends WhatsAppError {
886
+ readonly code: "AUTHENTICATION";
887
+ readonly metaCode: number | undefined;
888
+ readonly subcode: number | undefined;
889
+ constructor(message: string, meta?: AuthenticationErrorMeta, options?: WhatsAppErrorOptions);
890
+ }
891
+ interface PermissionErrorMeta {
892
+ /** Meta error code (200, 210, 230, 294, or 299 in v1). */
893
+ metaCode?: number;
894
+ }
895
+ declare class PermissionError extends WhatsAppError {
896
+ readonly code: "PERMISSION";
897
+ readonly metaCode: number | undefined;
898
+ constructor(message: string, meta?: PermissionErrorMeta, options?: WhatsAppErrorOptions);
899
+ }
900
+ interface CapabilityErrorMeta {
901
+ /** Meta error code (typically 100). */
902
+ metaCode?: number;
903
+ }
904
+ declare class CapabilityError extends WhatsAppError {
905
+ readonly code: "CAPABILITY";
906
+ readonly metaCode: number | undefined;
907
+ constructor(message: string, meta?: CapabilityErrorMeta, options?: WhatsAppErrorOptions);
908
+ }
909
+
910
+ /**
911
+ * Shared interface satisfied by both `WhatsAppClient` and
912
+ * `MockWhatsAppClient`. Consumer code that only needs send capability
913
+ * can take this union and run uniformly against either backend.
914
+ */
915
+ interface WhatsAppLikeClient {
916
+ readonly phoneNumberId: string;
917
+ readonly wabaId: string;
918
+ readonly graphApiVersion: GraphApiVersion;
919
+ isWindowOpen(to: string): Promise<boolean>;
920
+ sendText(input: BuildTextInput, options?: RequestOptions): Promise<MessageSendResponse>;
921
+ sendImage(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
922
+ sendVideo(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
923
+ sendAudio(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
924
+ sendDocument(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
925
+ sendSticker(input: BuildMediaInput, options?: RequestOptions): Promise<MessageSendResponse>;
926
+ sendLocation(input: BuildLocationInput, options?: RequestOptions): Promise<MessageSendResponse>;
927
+ sendContacts(input: BuildContactsInput, options?: RequestOptions): Promise<MessageSendResponse>;
928
+ sendInteractive(input: BuildInteractiveInput, options?: RequestOptions): Promise<MessageSendResponse>;
929
+ sendTemplate(input: BuildTemplateInput, options?: RequestOptions): Promise<MessageSendResponse>;
930
+ sendAuthTemplate(input: BuildAuthTemplateInput, options?: RequestOptions): Promise<MessageSendResponse>;
931
+ sendVoice(input: BuildVoiceInput, options?: RequestOptions): Promise<MessageSendResponse>;
932
+ sendCarouselTemplate(input: BuildCarouselTemplateInput, options?: RequestOptions): Promise<MessageSendResponse>;
933
+ sendReaction(input: BuildReactionInput, options?: RequestOptions): Promise<MessageSendResponse>;
934
+ sendReply(replyTo: string, payload: WhatsAppMessage, options?: RequestOptions): Promise<MessageSendResponse>;
935
+ listTemplates(query?: ListTemplatesQuery, options?: RequestOptions): Promise<ListTemplatesResponse>;
936
+ getTemplate(templateId: string, options?: RequestOptions): Promise<TemplateDefinition>;
937
+ }
938
+ /** A single send recorded by the mock client. */
939
+ interface RecordedSend {
940
+ wamid: string;
941
+ payload: WhatsAppMessage;
942
+ sentAt: number;
943
+ }
944
+ interface MockWhatsAppClientOptions {
945
+ phoneNumberId: string;
946
+ wabaId: string;
947
+ graphApiVersion?: GraphApiVersion;
948
+ windowTracker?: WindowTracker;
949
+ /** Optional clock injection (defaults to Date.now). */
950
+ now?: () => number;
951
+ /**
952
+ * Optional template registry. When supplied, `listTemplates(query?)`
953
+ * filters the seed in memory and `getTemplate(id)` resolves with the
954
+ * matching entry. When omitted, the mock preserves the v1 behaviour:
955
+ * `listTemplates` → `{ data: [] }` and `getTemplate` rejects.
956
+ */
957
+ templates?: ReadonlyArray<TemplateDefinition>;
958
+ }
959
+
960
+ /**
961
+ * In-memory implementation of `WhatsAppLikeClient`. Records every send
962
+ * as a `RecordedSend`, generates deterministic `wamid.mock-${counter}`
963
+ * ids, and never touches the network.
964
+ *
965
+ * Honours the same `windowTracker` gate as the real client.
966
+ */
967
+ declare class MockWhatsAppClient implements WhatsAppLikeClient {
968
+ #private;
969
+ readonly phoneNumberId: string;
970
+ readonly wabaId: string;
971
+ readonly graphApiVersion: GraphApiVersion;
972
+ constructor(options: MockWhatsAppClientOptions);
973
+ get sentMessages(): ReadonlyArray<RecordedSend>;
974
+ reset(): void;
975
+ isWindowOpen(to: string): Promise<boolean>;
976
+ /**
977
+ * Synthesise a webhook event into a `WebhookReceiver`. Bypasses
978
+ * signature verification — the receiver dispatches handlers directly.
979
+ */
980
+ simulateInbound(receiver: WebhookReceiver, event: WhatsAppEvent): Promise<void>;
981
+ sendText(input: BuildTextInput): Promise<MessageSendResponse>;
982
+ sendImage(input: BuildMediaInput): Promise<MessageSendResponse>;
983
+ sendVideo(input: BuildMediaInput): Promise<MessageSendResponse>;
984
+ sendAudio(input: BuildMediaInput): Promise<MessageSendResponse>;
985
+ sendDocument(input: BuildMediaInput): Promise<MessageSendResponse>;
986
+ sendSticker(input: BuildMediaInput): Promise<MessageSendResponse>;
987
+ sendLocation(input: BuildLocationInput): Promise<MessageSendResponse>;
988
+ sendContacts(input: BuildContactsInput): Promise<MessageSendResponse>;
989
+ sendInteractive(input: BuildInteractiveInput): Promise<MessageSendResponse>;
990
+ sendTemplate(input: BuildTemplateInput): Promise<MessageSendResponse>;
991
+ sendAuthTemplate(input: BuildAuthTemplateInput): Promise<MessageSendResponse>;
992
+ sendVoice(input: BuildVoiceInput): Promise<MessageSendResponse>;
993
+ sendCarouselTemplate(input: BuildCarouselTemplateInput): Promise<MessageSendResponse>;
994
+ sendReaction(input: BuildReactionInput): Promise<MessageSendResponse>;
995
+ sendReply(replyTo: string, payload: WhatsAppMessage): Promise<MessageSendResponse>;
996
+ listTemplates(query?: ListTemplatesQuery, _options?: RequestOptions): Promise<ListTemplatesResponse>;
997
+ getTemplate(templateId: string, _options?: RequestOptions): Promise<TemplateDefinition>;
998
+ }
999
+
1000
+ interface PickWhatsAppClientOptions extends WhatsAppClientOptions {
1001
+ /** Force the real client even when WHATSAPP_MODE=mock is set. */
1002
+ forceReal?: boolean;
1003
+ /** Force the mock client regardless of env. */
1004
+ forceMock?: boolean;
1005
+ /**
1006
+ * Optional template registry — only used when the factory routes to the
1007
+ * mock backend. Forwarded to `MockWhatsAppClient` as `options.templates`.
1008
+ * Ignored by the real client.
1009
+ */
1010
+ templates?: ReadonlyArray<TemplateDefinition>;
1011
+ }
1012
+ /**
1013
+ * Choose between the real `WhatsAppClient` and `MockWhatsAppClient`
1014
+ * based on `process.env.WHATSAPP_MODE`. Optional `forceReal` /
1015
+ * `forceMock` overrides take precedence over env detection.
1016
+ *
1017
+ * Returns the shared `WhatsAppLikeClient` interface so consumer code
1018
+ * runs uniformly against either implementation.
1019
+ */
1020
+ declare function pickWhatsAppClient(options: PickWhatsAppClientOptions): WhatsAppLikeClient;
1021
+
1022
+ /**
1023
+ * Set the salt used by `hashPhoneNumberId` (and friends) for PII redaction.
1024
+ * Production deployments SHOULD set this once at boot to a per-environment
1025
+ * value so spans across runs / replicas correlate consistently within an
1026
+ * environment but differ across environments.
1027
+ */
1028
+ declare function setRedactSalt(salt: string): void;
1029
+ /**
1030
+ * Stable, salted SHA-256 hash truncated to 16 lowercase-hex characters.
1031
+ * Suitable for tagging OTel spans without leaking the raw phone number id.
1032
+ *
1033
+ * Runtime-portable: uses `crypto.subtle.digest("SHA-256", ...)` so the
1034
+ * function runs unmodified on Node ≥ 20, Cloudflare Workers, Bun, Deno,
1035
+ * and any WinterCG runtime.
1036
+ */
1037
+ declare function hashPhoneNumberId(value: string): Promise<string>;
1038
+
1039
+ /**
1040
+ * Convenience accessor for the SDK's tracer. When no provider is
1041
+ * registered (default), the OTel API returns a no-op tracer and the
1042
+ * spans we emit are silent.
1043
+ */
1044
+ declare function getTracer(): Tracer;
1045
+ /**
1046
+ * Run `fn` inside an active OTel span. Applies `attributes` on start,
1047
+ * records exceptions, sets the span's status to ERROR on rejection,
1048
+ * and ends the span exactly once. Returns whatever `fn` resolves with.
1049
+ */
1050
+ declare function withSpan<T>(name: string, fn: () => Promise<T>, attributes?: Attributes): Promise<T>;
1051
+
1052
+ interface TokenBucketOptions {
1053
+ /** Maximum number of tokens the bucket holds. */
1054
+ capacity: number;
1055
+ /** Refill rate in tokens per millisecond (e.g. `80 / 1000` for 80 MPS). */
1056
+ refillPerMs: number;
1057
+ /** Optional clock injection (defaults to `Date.now`). */
1058
+ now?: () => number;
1059
+ }
1060
+ /**
1061
+ * Continuous-refill token bucket. `acquire(count)` resolves
1062
+ * immediately when enough tokens are available; otherwise waits
1063
+ * (via `setTimeout`) until refill produces enough. Concurrent
1064
+ * `acquire` calls on an empty bucket are serialised so two callers
1065
+ * never consume the same refilled token.
1066
+ *
1067
+ * No background timer is scheduled; refill is computed lazily on
1068
+ * access. Safe to construct and discard.
1069
+ */
1070
+ declare class TokenBucket {
1071
+ #private;
1072
+ constructor(options: TokenBucketOptions);
1073
+ /** Current token count after applying any pending refill. Pure read. */
1074
+ peek(): number;
1075
+ /** Last access timestamp (epoch ms). Used by `BucketMap` eviction. */
1076
+ lastAccessAt(): number;
1077
+ /** Whether the bucket currently has its full capacity worth of tokens. */
1078
+ isFull(): boolean;
1079
+ /**
1080
+ * Acquire `count` tokens. Resolves once the tokens are reserved
1081
+ * for this caller. Concurrent acquires queue behind the previous
1082
+ * waiter so the refill is consumed in arrival order.
1083
+ */
1084
+ acquire(count?: number): Promise<void>;
1085
+ }
1086
+
1087
+ interface BucketMapOptions extends TokenBucketOptions {
1088
+ /**
1089
+ * Buckets at full capacity AND idle for at least this many
1090
+ * milliseconds are evicted opportunistically on the next
1091
+ * `acquire`. Defaults to 60_000 (1 minute).
1092
+ */
1093
+ evictAfterMs?: number;
1094
+ }
1095
+ /**
1096
+ * Lazily-created map of `TokenBucket`s, keyed by an arbitrary
1097
+ * string. Buckets that have been full and idle for `evictAfterMs`
1098
+ * are dropped on the next `acquire` sweep so the map doesn't grow
1099
+ * unbounded under fanout workloads.
1100
+ *
1101
+ * No background timer is scheduled; eviction is opportunistic.
1102
+ */
1103
+ declare class BucketMap {
1104
+ #private;
1105
+ constructor(options: BucketMapOptions);
1106
+ acquire(key: string, count?: number): Promise<void>;
1107
+ size(): number;
1108
+ }
1109
+
1110
+ interface RateLimitOptions {
1111
+ /**
1112
+ * Per-pair (sender phone_number_id × recipient) ceiling.
1113
+ * Defaults to `{ messages: 1, per: 6_000 }` — Meta's documented
1114
+ * limit for unsolicited free-form sends.
1115
+ */
1116
+ perPair?: {
1117
+ messages: number;
1118
+ per: number;
1119
+ };
1120
+ /**
1121
+ * Per-WABA ceiling. Defaults to `{ mps: 80 }`, the verified-tier
1122
+ * starting limit. Raise as Meta grants higher tiers.
1123
+ */
1124
+ perWaba?: {
1125
+ mps: number;
1126
+ };
1127
+ /** Optional clock injection for deterministic testing. */
1128
+ now?: () => number;
1129
+ /**
1130
+ * Idle-eviction window for per-pair buckets. Buckets at full
1131
+ * capacity AND idle for ≥ this many ms are dropped. Default
1132
+ * 60_000.
1133
+ */
1134
+ evictAfterMs?: number;
1135
+ }
1136
+ /**
1137
+ * Decorate any `WhatsAppLikeClient` with two token-bucket rate
1138
+ * limiters so outbound sends respect Meta's per-pair and per-WABA
1139
+ * ceilings before issuing the HTTP request.
1140
+ *
1141
+ * The returned client has the same shape as the input. Non-send
1142
+ * methods (`isWindowOpen`, `listTemplates`, `getTemplate`) pass
1143
+ * through unchanged.
1144
+ */
1145
+ declare function withRateLimit(client: WhatsAppLikeClient, options?: RateLimitOptions): WhatsAppLikeClient;
1146
+
1147
+ /**
1148
+ * GET /{wabaId}/message_templates — list approved templates for a WABA.
1149
+ * The optional `query` is encoded as URL search params.
1150
+ */
1151
+ declare function listTemplates(client: WhatsAppClient, query?: ListTemplatesQuery, options?: RequestOptions): Promise<ListTemplatesResponse>;
1152
+ /** GET /{templateId} — fetch a single template definition by id. */
1153
+ declare function getTemplate(client: WhatsAppClient, templateId: string, options?: RequestOptions): Promise<TemplateDefinition>;
1154
+
1155
+ /**
1156
+ * Count the number of unique `{{N}}` placeholders in a template body / header
1157
+ * string. Validates that placeholders are 1-INDEXED and contiguous (no gaps).
1158
+ *
1159
+ * Throws `TemplateError` on:
1160
+ * - any `{{0}}` (Meta's variables are 1-indexed)
1161
+ * - gaps (e.g., `{{1}}` and `{{3}}` without a `{{2}}`)
1162
+ *
1163
+ * Repeated indices are counted once.
1164
+ */
1165
+ declare function countTemplatePlaceholders(text: string | undefined): number;
1166
+
1167
+ /**
1168
+ * Cross-validate a built `TemplateMessage` payload against an approved
1169
+ * `TemplateDefinition`. Throws `TemplateError(message, definition.name)`
1170
+ * on any mismatch:
1171
+ * 1. payload.template.name !== definition.name
1172
+ * 2. payload.template.language.code !== definition.language
1173
+ * 3. for each payload component: matching definition component must
1174
+ * exist (by type, and for buttons also by sub_type/index)
1175
+ * 4. parameter count must equal placeholder count in the matching
1176
+ * definition component
1177
+ */
1178
+ declare function validateTemplateSend(payload: TemplateMessage, definition: TemplateDefinition): void;
1179
+
1180
+ /**
1181
+ * Tracks which webhook events have been seen and returns whether the
1182
+ * current sighting is new. Backed by any `Storage` impl (in-memory,
1183
+ * Redis, etc.).
1184
+ *
1185
+ * The key for a `message` event is the wamid; for a `status` event the
1186
+ * receiver layers in the status string so transitions (sent → delivered
1187
+ * → read → failed) are not collapsed.
1188
+ */
1189
+ declare class WebhookDeduper {
1190
+ #private;
1191
+ constructor(storage: Storage, ttlMs?: number);
1192
+ markIfNew(eventKey: string): Promise<boolean>;
1193
+ }
1194
+
1195
+ interface VerifyHandshakeInput {
1196
+ /** `hub.mode` query param sent by Meta. */
1197
+ mode: string | null | undefined;
1198
+ /** `hub.verify_token` query param sent by Meta. */
1199
+ verifyToken: string | null | undefined;
1200
+ /** `hub.challenge` query param sent by Meta. */
1201
+ challenge: string | null | undefined;
1202
+ /** The verify-token shared secret you registered in Meta's webhook UI. */
1203
+ expectedToken: string;
1204
+ }
1205
+ /**
1206
+ * Verify Meta's GET-based webhook handshake. Returns the `challenge`
1207
+ * value when `mode === "subscribe"` AND `verifyToken === expectedToken`
1208
+ * (constant-time compare). Returns `null` for every other input — the
1209
+ * caller SHOULD respond `403 Forbidden`.
1210
+ *
1211
+ * Runtime-portable: uses an explicit constant-time byte-wise compare
1212
+ * rather than `node:crypto.timingSafeEqual`, so the function runs
1213
+ * unmodified on Cloudflare Workers, Bun, Deno, and any WinterCG runtime.
1214
+ */
1215
+ declare function verifyHandshake({ mode, verifyToken, challenge, expectedToken, }: VerifyHandshakeInput): string | null;
1216
+
1217
+ /**
1218
+ * Parse a Meta webhook payload (already JSON-decoded) into a flat
1219
+ * `ReadonlyArray<WhatsAppEvent>`. Pure, never throws, surfaces
1220
+ * unrecognised top-level fields as `{ kind: "unknown" }` so consumers
1221
+ * can log and the SDK can extend later without breaking changes.
1222
+ */
1223
+ declare function parseWebhookPayload(body: unknown): ReadonlyArray<WhatsAppEvent>;
1224
+
1225
+ interface VerifySignatureInput {
1226
+ /** Raw body BYTES Meta sent. Strings are UTF-8 encoded; do not pre-parse. */
1227
+ rawBody: Buffer | Uint8Array | string;
1228
+ /** The `X-Hub-Signature-256` header value. May or may not include the `sha256=` prefix. */
1229
+ signatureHeader: string | null | undefined;
1230
+ /** Your Meta app secret. */
1231
+ appSecret: string;
1232
+ }
1233
+ /**
1234
+ * Timing-safe HMAC-SHA256 verification of an incoming webhook body.
1235
+ *
1236
+ * Resolves to `true` if and only if the HMAC of the raw body, keyed with
1237
+ * `appSecret`, matches the hex value in `signatureHeader`. Resolves to
1238
+ * `false` (without rejecting) on every other input — including missing
1239
+ * header, malformed hex, or wrong byte length.
1240
+ *
1241
+ * Uses WebCrypto (`crypto.subtle.sign`) so the function runs unmodified
1242
+ * on Node ≥ 20, Cloudflare Workers, Bun, Deno, and any WinterCG runtime.
1243
+ */
1244
+ declare function verifySignature({ rawBody, signatureHeader, appSecret, }: VerifySignatureInput): Promise<boolean>;
1245
+ /**
1246
+ * Throwing variant of {@link verifySignature}. Resolves to `void` on a
1247
+ * valid signature; throws `WebhookSignatureError` on any failure (bad
1248
+ * HMAC, missing header, malformed hex, wrong byte length).
1249
+ *
1250
+ * Use this when wiring your own HTTP layer (i.e. not the SDK's
1251
+ * Express / web / Hono adapters) and you want to surface a typed
1252
+ * error rather than branch on a boolean. The SDK's bundled adapters
1253
+ * use the boolean variant and return `401` directly.
1254
+ */
1255
+ declare function verifySignatureOrThrow(input: VerifySignatureInput): Promise<void>;
1256
+ /**
1257
+ * Compute the lowercase-hex `HMAC-SHA256(appSecret, rawBody)`. Exposed
1258
+ * primarily so test fixtures can produce the expected header value.
1259
+ */
1260
+ declare function computeSignature(rawBody: Buffer | Uint8Array | string, appSecret: string): Promise<string>;
1261
+
1262
+ export { type AudioMessage, AuthenticationError, type AuthenticationErrorMeta, type BaseMessage, BucketMap, type BucketMapOptions, type BuildAuthTemplateInput, type BuildCarouselTemplateInput, type BuildContactsInput, type BuildInteractiveButtonInput, type BuildInteractiveCtaUrlInput, type BuildInteractiveInput, type BuildInteractiveListInput, type BuildLocationInput, type BuildMediaInput, type BuildReactionInput, type BuildTemplateInput, type BuildTextInput, type BuildVoiceInput, CapabilityError, type CapabilityErrorMeta, type CarouselCard, type CarouselCardButton, type CarouselCardComponent, type CarouselCardMediaHeader, type Contact, type ContactEmail, type ContactName, type ContactPhone, type ContactsMessage, type CredentialField, DEFAULT_RETRY_POLICY, type DocumentMessage, GRAPH_API_VERSION, type GraphApiVersion, type HttpMethod, type ImageMessage, type InteractiveBody, type InteractiveButtonAction, type InteractiveButtonBody, type InteractiveButtonReply, type InteractiveCtaUrlAction, type InteractiveCtaUrlBody, type InteractiveHeader, type InteractiveListAction, type InteractiveListBody, type InteractiveListRow, type InteractiveListSection, type InteractiveMessage, type ListTemplatesPaging, type ListTemplatesQuery, type ListTemplatesResponse, type LocationBody, type LocationMessage, META_GRAPH_BASE_URL, type MediaKind, type MediaSource, type MessageSendResponse, type MessageSendResponseContact, type MessageSendResponseMessage, MissingCredentialsError, MockModeError, MockWhatsAppClient, type MockWhatsAppClientOptions, PermissionError, type PermissionErrorMeta, type PickWhatsAppClientOptions, RateLimitError, type RateLimitErrorMeta, type RateLimitOptions, type ReactionBody, type ReactionMessage, type RecipientType, type RecordedSend, type RequestOptions, type RetryPolicy, type StickerMessage, Storage, type TemplateBody, type TemplateButtonDefinition, type TemplateCategory, type TemplateComponent, type TemplateComponentDefinition, type TemplateComponentDefinitionType, type TemplateDefinition, TemplateError, type TemplateLanguage, type TemplateMessage, type TemplateParameter, type TemplateParameterCouponCode, type TemplateParameterCurrency, type TemplateParameterDateTime, type TemplateParameterDocument, type TemplateParameterImage, type TemplateParameterLimitedTimeOffer, type TemplateParameterPayload, type TemplateParameterText, type TemplateParameterVideo, type TemplateStatus, type TextBody, type TextMessage, TokenBucket, type TokenBucketOptions, type TokenInfo, type TokenProvider, TransientHttpError, type VerifyHandshakeInput, type VerifySignatureInput, type VideoMessage, WEBHOOK_ACK_DEADLINE_MS, WEBHOOK_DEDUPE_TTL_MS, WINDOW_TTL_MS, WebhookDeduper, WebhookReceiver, WebhookSignatureError, WhatsAppClient, type WhatsAppClientOptions, WhatsAppError, type WhatsAppErrorCode, type WhatsAppErrorOptions, WhatsAppEvent, type WhatsAppLikeClient, type WhatsAppMessage, WindowClosedError, WindowTracker, type WindowTrackerOptions, buildAudio, buildAuthTemplate, buildCarouselTemplate, buildContacts, buildDocument, buildImage, buildInteractive, buildInteractiveButton, buildInteractiveCtaUrl, buildInteractiveList, buildLocation, buildReaction, buildSticker, buildTemplate, buildText, buildVideo, buildVoice, computeSignature, countTemplatePlaceholders, getTemplate, getTracer, hashPhoneNumberId, listTemplates, parseWebhookPayload, pickWhatsAppClient, sendMessage, setRedactSalt, validateTemplateSend, verifyHandshake, verifySignature, verifySignatureOrThrow, withRateLimit, withSpan };