interface Logger { debug(msg: string, meta?: Record): void; info(msg: string, meta?: Record): void; warn(msg: string, meta?: Record): void; error(msg: string, meta?: Record): void; } /** No-op logger used when no logger is provided */ declare const noopLogger: Logger; interface CloudApiConfig { provider: 'cloud-api'; /** The phone number ID from Meta Business Manager */ phoneNumberId: string; /** Permanent or temporary access token */ accessToken: string; /** Graph API version (default: "v25.0") */ apiVersion?: string; /** Token used when Meta sends the webhook verification GET request */ webhookVerifyToken?: string; /** App secret used to verify inbound webhook signatures (HMAC SHA-256) */ appSecret?: string; /** * WhatsApp Business Account ID — required for template management. * If not provided, template operations (list/create/delete) will throw. * Find it in Meta Business Manager → WhatsApp → Business Account Settings. */ wabaId?: string; } interface Dialog360Config { provider: '360dialog'; /** API key from the 360dialog dashboard */ apiKey: string; /** Override the default base URL (default: "https://waba-v2.360dialog.io") */ baseUrl?: string; /** Secret for webhook signature verification */ webhookSecret?: string; } interface WatiConfig { provider: 'wati'; /** Bearer token from Wati dashboard → Settings → API */ apiKey: string; /** Tenant-specific base URL (e.g. "https://live-mt-server.wati.io/300305") */ baseUrl: string; /** WATI sender channel number (example: "201234567890") */ channelNumber: string; /** Secret for webhook signature verification */ webhookSecret?: string; } /** Union of all provider configurations */ type ProviderConfig = CloudApiConfig | Dialog360Config | WatiConfig; interface RetryConfig { /** Maximum number of retry attempts (default: 3) */ maxRetries?: number; /** Base delay in ms before first retry (default: 1000) */ baseDelay?: number; /** Maximum delay in ms between retries (default: 30000) */ maxDelay?: number; /** * Retry non-idempotent requests (message sends, template creation, uploads) * after ambiguous failures (network error, timeout, or 5xx). * * Default `false`. Leaving this off prevents duplicate delivery — e.g. a * timed-out OTP send is NOT retried, so users never get two codes. Only * enable it if you have your own idempotency/dedup layer. * * Note: `429 Too Many Requests` is ALWAYS retried regardless of this flag, * because the request was rejected before processing (no duplicate risk). */ retryNonIdempotent?: boolean; } interface RateLimitConfig { /** Maximum requests per second (default: 80 for Cloud API) */ maxRequestsPerSecond?: number; /** * Maximum number of requests allowed to wait in the local queue before * `acquire()` rejects with a rate-limit error. Bounds memory under sustained * overload so a flood of concurrent sends can't grow the queue unbounded * (important on a single Workers isolate). Default: 10000. */ maxQueueSize?: number; /** * Maximum time (ms) a request may wait in the queue for a token before it * rejects with a `TimeoutError`. Prevents a request from hanging forever * before its fetch even starts. Default: 30000. */ queueTimeoutMs?: number; } interface ClientHooks { /** Called before every outbound HTTP request */ onRequest?: (info: { url: string; method: string; body?: unknown; }) => void; /** Called after every HTTP response */ onResponse?: (info: { url: string; status: number; durationMs: number; }) => void; /** Called on every error (after retries exhausted) */ onError?: (error: unknown) => void; } interface ClientOptions { /** Pluggable logger (default: no-op) */ logger?: Logger; /** Retry configuration */ retry?: RetryConfig; /** Rate limiting configuration */ rateLimit?: RateLimitConfig; /** Request timeout in milliseconds (default: 30_000) */ timeout?: number; /** Lifecycle hooks */ hooks?: ClientHooks; /** Include raw provider response in SendResult (default: false) */ includeRawResponse?: boolean; /** * Attach the raw webhook payload to each parsed event's `metadata.raw` * (default: false). Off by default so parsed events don't each retain the * full webhook body — important when a single POST yields many events and * they're queued for later processing. */ includeRawWebhook?: boolean; } /** Full config passed to `createWhatsApp()` */ type CreateWhatsAppConfig = ProviderConfig & ClientOptions; /** Phone number in E.164 format (e.g. "+966501234567") */ type PhoneNumber = string; /** Provider-issued message identifier */ type MessageId = string; /** Provider-issued media identifier */ type MediaId = string; /** ISO 8601 timestamp string */ type Timestamp = string; /** Supported provider identifiers */ type ProviderName = 'cloud-api' | '360dialog' | 'wati'; /** Result of any successful send operation */ interface SendResult { /** Provider-assigned message ID */ messageId: MessageId; /** Which provider handled this send */ provider: ProviderName; /** Raw provider response (opt-in for debugging) */ raw?: unknown; } /** Media source — either a public URL or a previously uploaded media ID */ type MediaSource = { url: string; id?: never; } | { id: MediaId; url?: never; }; /** Contact information for a WhatsApp user */ interface ContactInfo { name: string; waId: string; } /** Input for uploading media to the provider */ interface MediaUpload { /** File content as Uint8Array, Blob, or ReadableStream */ file: Uint8Array | Blob | ReadableStream; /** MIME type (e.g. "image/png", "application/pdf") */ mimeType: string; /** Optional filename */ filename?: string; } /** Result from a successful media upload */ interface MediaUploadResult { /** Provider-assigned media ID */ id: string; /** * Direct URL to the uploaded media, when the provider returns one (e.g. WATI). * Pass this URL to message sends. */ url?: string; } /** Result from getting a media URL */ interface MediaUrlResult { /** Download URL */ url: string; /** MIME type (when available) */ mimeType?: string; /** SHA-256 hash (when available) */ sha256?: string; /** File size in bytes (when available) */ fileSize?: number; /** When this URL expires (when available) */ expiresAt?: Date; } /** Result from downloading media — stream-based for memory efficiency */ interface MediaDownloadResult { /** ReadableStream for piping to storage (R2, S3, disk) */ stream: ReadableStream; /** MIME type of the downloaded content */ mimeType: string; /** Content length in bytes (when available from headers) */ contentLength?: number; } interface TextMessage { type: 'text'; to: PhoneNumber; text: { body: string; previewUrl?: boolean; }; /** Reply to a specific message */ context?: { messageId: string; }; } interface TemplateMessage { type: 'template'; to: PhoneNumber; template: { name: string; language: string; components?: TemplateComponent[]; }; } interface ImageMessage { type: 'image'; to: PhoneNumber; image: MediaSource & { caption?: string; }; context?: { messageId: string; }; } interface VideoMessage { type: 'video'; to: PhoneNumber; video: MediaSource & { caption?: string; }; context?: { messageId: string; }; } interface AudioMessage { type: 'audio'; to: PhoneNumber; audio: MediaSource; context?: { messageId: string; }; } interface DocumentMessage { type: 'document'; to: PhoneNumber; document: MediaSource & { caption?: string; filename?: string; }; context?: { messageId: string; }; } interface StickerMessage { type: 'sticker'; to: PhoneNumber; sticker: MediaSource; context?: { messageId: string; }; } interface LocationMessage { type: 'location'; to: PhoneNumber; location: { latitude: number; longitude: number; name?: string; address?: string; }; context?: { messageId: string; }; } interface ContactsMessage { type: 'contacts'; to: PhoneNumber; contacts: ContactPayload[]; context?: { messageId: string; }; } interface ReactionMessage { type: 'reaction'; to: PhoneNumber; reaction: { /** The message ID to react to */ messageId: string; /** Emoji to react with, or empty string to remove reaction */ emoji: string; }; } interface InteractiveButtonMessage { type: 'interactive.button'; to: PhoneNumber; body: string; header?: InteractiveHeader; footer?: string; /** Maximum 3 buttons */ buttons: ButtonDef[]; context?: { messageId: string; }; } interface InteractiveListMessage { type: 'interactive.list'; to: PhoneNumber; body: string; buttonText: string; header?: string; footer?: string; /** Maximum 10 sections */ sections: SectionDef[]; context?: { messageId: string; }; } type OutboundMessage = TextMessage | TemplateMessage | ImageMessage | VideoMessage | AudioMessage | DocumentMessage | StickerMessage | LocationMessage | ContactsMessage | ReactionMessage | InteractiveButtonMessage | InteractiveListMessage; interface ButtonDef { id: string; title: string; } interface SectionDef { title: string; rows: SectionRow[]; } interface SectionRow { id: string; title: string; description?: string; } type InteractiveHeader = { type: 'text'; text: string; } | { type: 'image'; image: MediaSource; } | { type: 'video'; video: MediaSource; } | { type: 'document'; document: MediaSource; }; interface TemplateComponent { type: 'header' | 'body' | 'button'; sub_type?: 'quick_reply' | 'url'; index?: number; parameters: TemplateParameter[]; } type TemplateParameter = { type: 'text'; text: string; name?: string; } | { type: 'currency'; currency: CurrencyParam; } | { type: 'date_time'; date_time: { fallback_value: string; }; } | { type: 'image'; image: MediaSource; } | { type: 'video'; video: MediaSource; } | { type: 'document'; document: MediaSource; } | { type: 'payload'; payload: string; }; interface CurrencyParam { fallback_value: string; code: string; amount_1000: number; } interface ContactPayload { name: { formatted_name: string; first_name?: string; last_name?: string; middle_name?: string; prefix?: string; suffix?: string; }; phones?: Array<{ phone: string; type?: 'CELL' | 'MAIN' | 'IPHONE' | 'HOME' | 'WORK'; wa_id?: string; }>; emails?: Array<{ email: string; type?: 'HOME' | 'WORK'; }>; urls?: Array<{ url: string; type?: 'HOME' | 'WORK'; }>; addresses?: Array<{ street?: string; city?: string; state?: string; zip?: string; country?: string; country_code?: string; type?: 'HOME' | 'WORK'; }>; org?: { company?: string; department?: string; title?: string; }; birthday?: string; } interface TextOptions { previewUrl?: boolean; replyTo?: string; } interface MediaMessageOptions { caption?: string; replyTo?: string; } interface DocumentOptions extends MediaMessageOptions { filename?: string; } interface InteractiveOptions { header?: InteractiveHeader; footer?: string; replyTo?: string; } interface LocationPayload { latitude: number; longitude: number; name?: string; address?: string; } interface OtpSendOptions { /** The approved AUTHENTICATION template name to send the code with. */ template: string; /** BCP-47 language code of the template (default: "en_US"). */ language?: string; /** * Also fill the template's copy-code / one-tap autofill button with the code * (default: true). Set false if your authentication template has no button. * Ignored by providers whose templates only use a body placeholder (Wati). */ button?: boolean; } /** A WhatsApp message template */ interface Template { id: string; name: string; language: string; status: 'APPROVED' | 'PENDING' | 'REJECTED' | 'DISABLED' | 'PAUSED'; category: 'UTILITY' | 'MARKETING' | 'AUTHENTICATION'; components: TemplateComponentDef[]; } interface TemplateComponentDef { type: 'HEADER' | 'BODY' | 'FOOTER' | 'BUTTONS'; format?: 'TEXT' | 'IMAGE' | 'VIDEO' | 'DOCUMENT'; text?: string; buttons?: TemplateButtonDef[]; example?: { header_text?: string[]; body_text?: string[][]; header_handle?: string[]; }; /** * AUTHENTICATION templates only — adds the "this code is for you" security * disclaimer to the BODY. Passed through verbatim to the provider. */ add_security_recommendation?: boolean; /** * AUTHENTICATION templates only — minutes until the code expires, shown in * the FOOTER. Passed through verbatim to the provider. */ code_expiration_minutes?: number; } interface TemplateButtonDef { /** `OTP` is used by AUTHENTICATION templates (copy-code / one-tap autofill). */ type: 'PHONE_NUMBER' | 'URL' | 'QUICK_REPLY' | 'OTP'; text?: string; phone_number?: string; url?: string; example?: string[]; /** OTP buttons only — the autofill behaviour. */ otp_type?: 'COPY_CODE' | 'ONE_TAP' | 'ZERO_TAP'; /** ONE_TAP/ZERO_TAP OTP buttons only — Android app integration fields. */ autofill_text?: string; package_name?: string; signature_hash?: string; } /** Input for creating a new template */ interface CreateTemplateInput { name: string; language: string; category: 'UTILITY' | 'MARKETING' | 'AUTHENTICATION'; components: TemplateComponentDef[]; } type WebhookEvent = IncomingMessageEvent | MessageStatusEvent | MessageErrorEvent; interface IncomingMessageEvent { type: 'message'; /** Provider-assigned message ID */ messageId: string; /** Sender phone number in E.164 format */ from: string; /** When the message was sent */ timestamp: Date; /** The actual message content */ message: IncomingMessage; /** Sender contact info (when available) */ contact?: { name: string; waId: string; }; /** Provider metadata */ metadata: WebhookMetadata; } /** Discriminated union of all possible incoming message shapes */ type IncomingMessage = IncomingTextMessage | IncomingImageMessage | IncomingVideoMessage | IncomingAudioMessage | IncomingDocumentMessage | IncomingLocationMessage | IncomingStickerMessage | IncomingReactionMessage | IncomingButtonReply | IncomingListReply | IncomingContactsMessage | IncomingUnknownMessage; interface IncomingTextMessage { type: 'text'; body: string; } interface IncomingImageMessage { type: 'image'; mediaId: string; mimeType: string; sha256?: string; caption?: string; } interface IncomingVideoMessage { type: 'video'; mediaId: string; mimeType: string; sha256?: string; caption?: string; } interface IncomingAudioMessage { type: 'audio'; mediaId: string; mimeType: string; sha256?: string; voice?: boolean; } interface IncomingDocumentMessage { type: 'document'; mediaId: string; mimeType: string; sha256?: string; filename?: string; caption?: string; } interface IncomingLocationMessage { type: 'location'; latitude: number; longitude: number; name?: string; address?: string; } interface IncomingStickerMessage { type: 'sticker'; mediaId: string; mimeType: string; animated: boolean; } interface IncomingReactionMessage { type: 'reaction'; emoji: string; reactedMessageId: string; } interface IncomingButtonReply { type: 'button_reply'; buttonId: string; title: string; } interface IncomingListReply { type: 'list_reply'; listId: string; title: string; description?: string; } interface IncomingContactsMessage { type: 'contacts'; contacts: Array<{ name: { formatted_name: string; first_name?: string; last_name?: string; }; phones?: Array<{ phone: string; wa_id?: string; type?: string; }>; }>; } interface IncomingUnknownMessage { type: 'unknown'; /** Raw payload for debugging */ raw: unknown; } interface MessageStatusEvent { type: 'status'; /** The message ID this status relates to */ messageId: string; /** Delivery status */ status: 'sent' | 'delivered' | 'read' | 'failed'; /** Recipient phone number */ recipientId: string; /** When this status was recorded */ timestamp: Date; /** Error details (only when status === 'failed') */ errors?: Array<{ code: number; title: string; message?: string; }>; /** Provider metadata */ metadata: WebhookMetadata; } interface MessageErrorEvent { type: 'error'; code: number; title: string; message: string; metadata: WebhookMetadata; } interface WebhookMetadata { /** Which provider this webhook came from */ provider: ProviderName; /** Phone number ID (when available) */ phoneNumberId?: string; /** Display phone number (when available) */ displayPhoneNumber?: string; /** * Raw webhook payload for debugging. Only populated when the client is * created with `includeRawWebhook: true` (otherwise `undefined` to avoid * retaining the full body on every event). */ raw?: unknown; } /** Features that a provider may or may not support */ type ProviderFeature = 'interactive.button' | 'interactive.list' | 'media.upload' | 'media.download' | 'media.delete' | 'template.management' | 'reaction' | 'read_receipts' | 'sticker' | 'location' | 'contacts' | 'webhook.signature_verification' | 'webhook.challenge'; /** * The contract every WhatsApp provider adapter must implement. * * The `WhatsAppClient` delegates all operations to the active adapter. * To add a new provider, implement this interface and register it * in the provider factory map. */ interface WhatsAppProviderAdapter { /** Provider identifier (e.g. "cloud-api", "360dialog", "wati") */ readonly name: string; /** Send any outbound message type */ sendMessage(message: OutboundMessage): Promise; /** Mark a message as read (send read receipt) */ markAsRead(messageId: string): Promise; /** Upload media to the provider */ uploadMedia(params: MediaUpload): Promise; /** Get the download URL for a media item */ getMediaUrl(mediaId: string): Promise; /** Download media as a ReadableStream */ downloadMedia(mediaIdOrUrl: string): Promise; /** Delete a previously uploaded media item */ deleteMedia(mediaId: string): Promise; /** Parse a raw webhook payload into normalized events */ parseWebhook(body: unknown): WebhookEvent[]; /** * Verify the cryptographic signature of a webhook payload. * Uses Web Crypto API (crypto.subtle) — no node:crypto. */ verifyWebhookSignature(body: string, signature: string): Promise; /** * Handle the webhook verification challenge (GET request). * Returns the challenge string if valid, null if invalid. * Only applicable to providers that use challenge-response (Cloud API). */ handleVerificationChallenge?(query: Record): string | null; /** List all message templates */ listTemplates?(): Promise; /** Create a new message template */ createTemplate?(input: CreateTemplateInput): Promise