whatsapp-cloud 0.0.4 → 0.0.6

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/agent_docs/INCOMING_MESSAGES_BRAINSTORM.md +500 -0
  3. package/cloud-api-docs/webhooks/endpoint.md +112 -0
  4. package/cloud-api-docs/webhooks/overview.md +154 -0
  5. package/docs/DEVELOPMENT.md +154 -0
  6. package/docs/webhooks.md +76 -0
  7. package/package.json +6 -2
  8. package/src/client/HttpClient.ts +43 -6
  9. package/src/client/WhatsAppClient.ts +6 -0
  10. package/src/examples/main.ts +9 -0
  11. package/src/examples/template.ts +134 -0
  12. package/src/index.ts +7 -0
  13. package/src/schemas/client.ts +2 -2
  14. package/src/schemas/index.ts +2 -0
  15. package/src/schemas/templates/component.ts +145 -0
  16. package/src/schemas/templates/index.ts +4 -0
  17. package/src/schemas/templates/request.ts +78 -0
  18. package/src/schemas/templates/response.ts +64 -0
  19. package/src/schemas/webhooks/incoming-message.ts +38 -0
  20. package/src/schemas/webhooks/index.ts +3 -0
  21. package/src/schemas/webhooks/payload.ts +56 -0
  22. package/src/services/accounts/AccountsClient.ts +6 -14
  23. package/src/services/accounts/AccountsService.ts +19 -21
  24. package/src/services/accounts/methods/list-phone-numbers.ts +1 -2
  25. package/src/services/business/BusinessClient.ts +1 -9
  26. package/src/services/business/BusinessService.ts +19 -21
  27. package/src/services/business/methods/list-accounts.ts +1 -2
  28. package/src/services/messages/MessagesClient.ts +2 -6
  29. package/src/services/messages/MessagesService.ts +42 -22
  30. package/src/services/templates/TemplatesClient.ts +35 -0
  31. package/src/services/templates/TemplatesService.ts +117 -0
  32. package/src/services/templates/index.ts +3 -0
  33. package/src/services/templates/methods/create.ts +27 -0
  34. package/src/services/templates/methods/delete.ts +38 -0
  35. package/src/services/templates/methods/get.ts +23 -0
  36. package/src/services/templates/methods/list.ts +36 -0
  37. package/src/services/templates/methods/update.ts +35 -0
  38. package/src/services/webhooks/WebhooksService.ts +214 -0
  39. package/src/services/webhooks/index.ts +3 -0
  40. package/src/services/webhooks/utils/extract-messages.ts +25 -0
  41. package/src/services/webhooks/utils/extract-statuses.ts +25 -0
  42. package/src/services/webhooks/utils/verify.ts +29 -0
  43. package/src/types/index.ts +2 -0
  44. package/src/types/templates/component.ts +33 -0
  45. package/src/types/templates/index.ts +4 -0
  46. package/src/types/templates/request.ts +28 -0
  47. package/src/types/templates/response.ts +34 -0
  48. package/src/types/webhooks/incoming-message.ts +16 -0
  49. package/src/types/webhooks/index.ts +3 -0
  50. package/src/types/webhooks/payload.ts +8 -0
  51. package/tsconfig.json +2 -3
@@ -1,4 +1,4 @@
1
- import type { HttpClient } from "../../client/HttpClient";
1
+ import { HttpClient } from "../../client/HttpClient";
2
2
  import { sendText } from "./methods/send-text";
3
3
  import { sendImage } from "./methods/send-image";
4
4
  import { sendLocation } from "./methods/send-location";
@@ -16,62 +16,82 @@ import type { MessageResponse } from "../../types/messages/response";
16
16
  /**
17
17
  * Messages service for sending WhatsApp messages
18
18
  *
19
- * The service validates that phoneNumberId is set at the client level and creates
20
- * a MessagesClient instance. MessagesClient treats phoneNumberId as a "client" for
21
- * the messaging namespace - different phoneNumberIds represent different endpoints.
19
+ * This service handles message operations.
20
+ * It supports both a globally configured phoneNumberId (in WhatsAppClient)
21
+ * and per-request phoneNumberId overrides.
22
22
  */
23
23
  export class MessagesService {
24
- private readonly messagesClient: MessagesClient;
24
+ constructor(private readonly httpClient: HttpClient) {}
25
25
 
26
- constructor(httpClient: HttpClient) {
27
- // Validate that phoneNumberId is set at client level
28
- if (!httpClient.phoneNumberId) {
26
+ /**
27
+ * Helper to create a Scoped Client (prefer override, fallback to config)
28
+ */
29
+ private getClient(overrideId?: string): MessagesClient {
30
+ const id = overrideId || this.httpClient.phoneNumberId;
31
+ if (!id) {
29
32
  throw new WhatsAppValidationError(
30
- "phoneNumberId is required for MessagesService. Provide it in WhatsAppClient config.",
33
+ "phoneNumberId is required. Provide it in WhatsAppClient config or as a parameter.",
31
34
  "phoneNumberId"
32
35
  );
33
36
  }
34
37
 
35
- // Create messages client with phone number ID baked in
36
- this.messagesClient = new MessagesClient(
37
- httpClient,
38
- httpClient.phoneNumberId
39
- );
38
+ // Just wrap the existing httpClient
39
+ return new MessagesClient(this.httpClient, id);
40
40
  }
41
41
 
42
42
  /**
43
43
  * Send a text message
44
44
  *
45
45
  * @param request - Text message request (to, text)
46
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
46
47
  */
47
- async sendText(request: SendTextRequest): Promise<MessageResponse> {
48
- return sendText(this.messagesClient, request);
48
+ async sendText(
49
+ request: SendTextRequest,
50
+ phoneNumberId?: string
51
+ ): Promise<MessageResponse> {
52
+ const client = this.getClient(phoneNumberId);
53
+ return sendText(client, request);
49
54
  }
50
55
 
51
56
  /**
52
57
  * Send an image message
53
58
  *
54
59
  * @param request - Image message request (to, image)
60
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
55
61
  */
56
- async sendImage(request: SendImageRequest): Promise<MessageResponse> {
57
- return sendImage(this.messagesClient, request);
62
+ async sendImage(
63
+ request: SendImageRequest,
64
+ phoneNumberId?: string
65
+ ): Promise<MessageResponse> {
66
+ const client = this.getClient(phoneNumberId);
67
+ return sendImage(client, request);
58
68
  }
59
69
 
60
70
  /**
61
71
  * Send a location message
62
72
  *
63
73
  * @param request - Location message request (to, location)
74
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
64
75
  */
65
- async sendLocation(request: SendLocationRequest): Promise<MessageResponse> {
66
- return sendLocation(this.messagesClient, request);
76
+ async sendLocation(
77
+ request: SendLocationRequest,
78
+ phoneNumberId?: string
79
+ ): Promise<MessageResponse> {
80
+ const client = this.getClient(phoneNumberId);
81
+ return sendLocation(client, request);
67
82
  }
68
83
 
69
84
  /**
70
85
  * Send a reaction message
71
86
  *
72
87
  * @param request - Reaction message request (to, reaction)
88
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
73
89
  */
74
- async sendReaction(request: SendReactionRequest): Promise<MessageResponse> {
75
- return sendReaction(this.messagesClient, request);
90
+ async sendReaction(
91
+ request: SendReactionRequest,
92
+ phoneNumberId?: string
93
+ ): Promise<MessageResponse> {
94
+ const client = this.getClient(phoneNumberId);
95
+ return sendReaction(client, request);
76
96
  }
77
97
  }
@@ -0,0 +1,35 @@
1
+ import type { HttpClient } from "../../client/HttpClient";
2
+
3
+ /**
4
+ * Templates client - wraps HttpClient with WABA ID as base endpoint
5
+ *
6
+ * This client automatically prepends `/${businessAccountId}` to all request paths.
7
+ * Similar to AccountsClient, since templates are scoped to WABA.
8
+ */
9
+ export class TemplatesClient {
10
+ constructor(
11
+ private readonly httpClient: HttpClient,
12
+ private readonly businessAccountId: string
13
+ ) {}
14
+
15
+ /**
16
+ * Make a GET request with WABA ID prefix
17
+ */
18
+ async get<T>(path: string): Promise<T> {
19
+ return this.httpClient.get<T>(`/${this.businessAccountId}${path}`);
20
+ }
21
+
22
+ /**
23
+ * Make a POST request with WABA ID prefix
24
+ */
25
+ async post<T>(path: string, body: unknown): Promise<T> {
26
+ return this.httpClient.post<T>(`/${this.businessAccountId}${path}`, body);
27
+ }
28
+
29
+ /**
30
+ * Make a DELETE request with WABA ID prefix
31
+ */
32
+ async delete<T>(path: string): Promise<T> {
33
+ return this.httpClient.delete<T>(`/${this.businessAccountId}${path}`);
34
+ }
35
+ }
@@ -0,0 +1,117 @@
1
+ import type { HttpClient } from "../../client/HttpClient";
2
+ import { TemplatesClient } from "./TemplatesClient";
3
+ import { createTemplate } from "./methods/create";
4
+ import { listTemplates } from "./methods/list";
5
+ import { getTemplate } from "./methods/get";
6
+ import { updateTemplate } from "./methods/update";
7
+ import { deleteTemplate } from "./methods/delete";
8
+ import { WhatsAppValidationError } from "../../errors";
9
+ import type {
10
+ CreateTemplateRequest,
11
+ UpdateTemplateRequest,
12
+ ListTemplatesRequest,
13
+ DeleteTemplateRequest,
14
+ } from "../../types/templates/request";
15
+ import type {
16
+ CreateTemplateResponse,
17
+ ListTemplatesResponse,
18
+ TemplateResponse,
19
+ UpdateTemplateResponse,
20
+ DeleteTemplateResponse,
21
+ } from "../../types/templates/response";
22
+
23
+ /**
24
+ * Templates service for managing message templates
25
+ *
26
+ * This service handles template operations like creating, listing, and deleting templates.
27
+ * It supports both a globally configured businessAccountId (in WhatsAppClient)
28
+ * and per-request businessAccountId overrides.
29
+ *
30
+ * Note: Get and Update operations use template ID directly (no WABA prefix needed).
31
+ */
32
+ export class TemplatesService {
33
+ constructor(private readonly httpClient: HttpClient) {}
34
+
35
+ /**
36
+ * Helper to create a Scoped Client (prefer override, fallback to config)
37
+ */
38
+ private getClient(overrideId?: string): TemplatesClient {
39
+ const id = overrideId || this.httpClient.businessAccountId;
40
+ if (!id) {
41
+ throw new WhatsAppValidationError(
42
+ "businessAccountId (WABA ID) is required for templates. Provide it in WhatsAppClient config or as a parameter.",
43
+ "businessAccountId"
44
+ );
45
+ }
46
+
47
+ return new TemplatesClient(this.httpClient, id);
48
+ }
49
+
50
+ /**
51
+ * Create a message template
52
+ *
53
+ * @param request - Template creation request
54
+ * @param businessAccountId - Optional WABA ID (overrides client config)
55
+ */
56
+ async create(
57
+ request: CreateTemplateRequest,
58
+ businessAccountId?: string
59
+ ): Promise<CreateTemplateResponse> {
60
+ const client = this.getClient(businessAccountId);
61
+ return createTemplate(client, request);
62
+ }
63
+
64
+ /**
65
+ * List message templates
66
+ *
67
+ * @param options - Optional filter options (name)
68
+ * @param businessAccountId - Optional WABA ID (overrides client config)
69
+ */
70
+ async list(
71
+ options?: ListTemplatesRequest,
72
+ businessAccountId?: string
73
+ ): Promise<ListTemplatesResponse> {
74
+ const client = this.getClient(businessAccountId);
75
+ return listTemplates(client, options);
76
+ }
77
+
78
+ /**
79
+ * Get a template by ID
80
+ *
81
+ * Note: This uses the template ID directly (no WABA prefix needed)
82
+ *
83
+ * @param templateId - Template ID
84
+ */
85
+ async get(templateId: string): Promise<TemplateResponse> {
86
+ return getTemplate(this.httpClient, templateId);
87
+ }
88
+
89
+ /**
90
+ * Update a template
91
+ *
92
+ * Note: This uses the template ID directly (no WABA prefix needed)
93
+ *
94
+ * @param templateId - Template ID
95
+ * @param request - Template update request
96
+ */
97
+ async update(
98
+ templateId: string,
99
+ request: UpdateTemplateRequest
100
+ ): Promise<UpdateTemplateResponse> {
101
+ return updateTemplate(this.httpClient, templateId, request);
102
+ }
103
+
104
+ /**
105
+ * Delete a template
106
+ *
107
+ * @param options - Delete options (name or hsm_id)
108
+ * @param businessAccountId - Optional WABA ID (overrides client config)
109
+ */
110
+ async delete(
111
+ options: DeleteTemplateRequest,
112
+ businessAccountId?: string
113
+ ): Promise<DeleteTemplateResponse> {
114
+ const client = this.getClient(businessAccountId);
115
+ return deleteTemplate(client, options);
116
+ }
117
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./TemplatesService";
2
+ export * from "./TemplatesClient";
3
+
@@ -0,0 +1,27 @@
1
+ import type { TemplatesClient } from "../TemplatesClient";
2
+ import { createTemplateRequestSchema } from "../../../schemas/templates/request";
3
+ import type { CreateTemplateRequest } from "../../../types/templates/request";
4
+ import type { CreateTemplateResponse } from "../../../types/templates/response";
5
+ import { transformZodError } from "../../../utils/zod-error";
6
+
7
+ /**
8
+ * Create a message template
9
+ *
10
+ * @param templatesClient - Templates client with WABA ID baked in
11
+ * @param request - Template creation request
12
+ */
13
+ export async function createTemplate(
14
+ templatesClient: TemplatesClient,
15
+ request: CreateTemplateRequest
16
+ ): Promise<CreateTemplateResponse> {
17
+ // Validate request with schema - throws WhatsAppValidationError if invalid
18
+ const result = createTemplateRequestSchema.safeParse(request);
19
+ if (!result.success) {
20
+ throw transformZodError(result.error);
21
+ }
22
+ const data = result.data;
23
+
24
+ // Make API request - templatesClient handles the WABA ID prefix automatically
25
+ return templatesClient.post<CreateTemplateResponse>("/message_templates", data);
26
+ }
27
+
@@ -0,0 +1,38 @@
1
+ import type { TemplatesClient } from "../TemplatesClient";
2
+ import { deleteTemplateRequestSchema } from "../../../schemas/templates/request";
3
+ import type { DeleteTemplateRequest } from "../../../types/templates/request";
4
+ import type { DeleteTemplateResponse } from "../../../types/templates/response";
5
+ import { transformZodError } from "../../../utils/zod-error";
6
+
7
+ /**
8
+ * Delete a template
9
+ *
10
+ * @param templatesClient - Templates client with WABA ID baked in
11
+ * @param options - Delete options (name or hsm_id)
12
+ */
13
+ export async function deleteTemplate(
14
+ templatesClient: TemplatesClient,
15
+ options: DeleteTemplateRequest
16
+ ): Promise<DeleteTemplateResponse> {
17
+ // Validate request with schema - throws WhatsAppValidationError if invalid
18
+ const result = deleteTemplateRequestSchema.safeParse(options);
19
+ if (!result.success) {
20
+ throw transformZodError(result.error);
21
+ }
22
+ const data = result.data;
23
+
24
+ // Build query string
25
+ const params = new URLSearchParams();
26
+ if (data.name) {
27
+ params.append("name", data.name);
28
+ }
29
+ if (data.hsm_id) {
30
+ params.append("hsm_id", data.hsm_id);
31
+ }
32
+ const queryString = params.toString();
33
+ const path = `/message_templates?${queryString}`;
34
+
35
+ // Make API request - templatesClient handles the WABA ID prefix automatically
36
+ return templatesClient.delete<DeleteTemplateResponse>(path);
37
+ }
38
+
@@ -0,0 +1,23 @@
1
+ import type { HttpClient } from "../../../client/HttpClient";
2
+ import type { TemplateResponse } from "../../../types/templates/response";
3
+
4
+ /**
5
+ * Get a template by ID
6
+ *
7
+ * Note: This uses the template ID directly (no WABA prefix needed)
8
+ *
9
+ * @param httpClient - HTTP client
10
+ * @param templateId - Template ID
11
+ */
12
+ export async function getTemplate(
13
+ httpClient: HttpClient,
14
+ templateId: string
15
+ ): Promise<TemplateResponse> {
16
+ if (!templateId || templateId.trim().length === 0) {
17
+ throw new Error("Template ID is required");
18
+ }
19
+
20
+ // Make API request - template ID is used directly, no WABA prefix
21
+ return httpClient.get<TemplateResponse>(`/${templateId}`);
22
+ }
23
+
@@ -0,0 +1,36 @@
1
+ import type { TemplatesClient } from "../TemplatesClient";
2
+ import { listTemplatesRequestSchema } from "../../../schemas/templates/request";
3
+ import type { ListTemplatesRequest } from "../../../types/templates/request";
4
+ import type { ListTemplatesResponse } from "../../../types/templates/response";
5
+ import { transformZodError } from "../../../utils/zod-error";
6
+
7
+ /**
8
+ * List message templates
9
+ *
10
+ * @param templatesClient - Templates client with WABA ID baked in
11
+ * @param options - Optional filter options (name)
12
+ */
13
+ export async function listTemplates(
14
+ templatesClient: TemplatesClient,
15
+ options?: ListTemplatesRequest
16
+ ): Promise<ListTemplatesResponse> {
17
+ // Validate options if provided
18
+ if (options) {
19
+ const result = listTemplatesRequestSchema.safeParse(options);
20
+ if (!result.success) {
21
+ throw transformZodError(result.error);
22
+ }
23
+ }
24
+
25
+ // Build query string
26
+ const params = new URLSearchParams();
27
+ if (options?.name) {
28
+ params.append("name", options.name);
29
+ }
30
+ const queryString = params.toString();
31
+ const path = queryString ? `/message_templates?${queryString}` : "/message_templates";
32
+
33
+ // Make API request - templatesClient handles the WABA ID prefix automatically
34
+ return templatesClient.get<ListTemplatesResponse>(path);
35
+ }
36
+
@@ -0,0 +1,35 @@
1
+ import type { HttpClient } from "../../../client/HttpClient";
2
+ import { updateTemplateRequestSchema } from "../../../schemas/templates/request";
3
+ import type { UpdateTemplateRequest } from "../../../types/templates/request";
4
+ import type { UpdateTemplateResponse } from "../../../types/templates/response";
5
+ import { transformZodError } from "../../../utils/zod-error";
6
+
7
+ /**
8
+ * Update a template
9
+ *
10
+ * Note: This uses the template ID directly (no WABA prefix needed)
11
+ *
12
+ * @param httpClient - HTTP client
13
+ * @param templateId - Template ID
14
+ * @param request - Template update request
15
+ */
16
+ export async function updateTemplate(
17
+ httpClient: HttpClient,
18
+ templateId: string,
19
+ request: UpdateTemplateRequest
20
+ ): Promise<UpdateTemplateResponse> {
21
+ if (!templateId || templateId.trim().length === 0) {
22
+ throw new Error("Template ID is required");
23
+ }
24
+
25
+ // Validate request with schema - throws WhatsAppValidationError if invalid
26
+ const result = updateTemplateRequestSchema.safeParse(request);
27
+ if (!result.success) {
28
+ throw transformZodError(result.error);
29
+ }
30
+ const data = result.data;
31
+
32
+ // Make API request - template ID is used directly, no WABA prefix
33
+ return httpClient.post<UpdateTemplateResponse>(`/${templateId}`, data);
34
+ }
35
+
@@ -0,0 +1,214 @@
1
+ import type { HttpClient } from "../../client/HttpClient";
2
+ import { extractMessages } from "./utils/extract-messages";
3
+ import { extractStatuses } from "./utils/extract-statuses";
4
+ import { verifyWebhook } from "./utils/verify";
5
+ import { webhookPayloadSchema } from "../../schemas/webhooks/payload";
6
+ import type {
7
+ WebhookPayload,
8
+ IncomingTextMessage,
9
+ IncomingMessage,
10
+ } from "../../types/webhooks";
11
+
12
+ /**
13
+ * Context provided to message handlers
14
+ * Contains metadata and contact info (message is passed separately)
15
+ */
16
+ export type MessageContext = {
17
+ metadata: {
18
+ phoneNumberId: string;
19
+ displayPhoneNumber: string;
20
+ wabaId: string;
21
+ };
22
+ contact?: {
23
+ name: string;
24
+ waId: string;
25
+ };
26
+ };
27
+
28
+ /**
29
+ * Handler functions for different message types
30
+ * Receives message and context separately - message is the focus, context is optional metadata
31
+ */
32
+ export type MessageHandlers = {
33
+ text?: (
34
+ message: IncomingTextMessage,
35
+ context: MessageContext
36
+ ) => Promise<void> | void;
37
+ // Future: image, audio, video, etc.
38
+ };
39
+
40
+ /**
41
+ * Options for handle() method
42
+ */
43
+ export type HandleOptions = {
44
+ /**
45
+ * Error handler called when a message handler throws an error
46
+ * If not provided, errors are logged and processing continues
47
+ */
48
+ onError?: (error: Error, message: IncomingMessage) => void;
49
+ };
50
+
51
+ /**
52
+ * Webhooks service for handling incoming webhook payloads
53
+ *
54
+ * Provides utilities for extracting messages and a convenience handler
55
+ * for type-safe message processing.
56
+ */
57
+ export class WebhooksService {
58
+ constructor(private readonly httpClient: HttpClient) {}
59
+
60
+ /**
61
+ * Verify webhook GET request from Meta
62
+ *
63
+ * Meta sends GET requests to verify webhook endpoints during setup.
64
+ * Returns the challenge string if valid, null if invalid.
65
+ *
66
+ * @param query - Query parameters from GET request
67
+ * @param verifyToken - Your verification token (stored on your server)
68
+ * @returns Challenge string if valid, null if invalid
69
+ */
70
+ verify(
71
+ query: {
72
+ "hub.mode"?: string;
73
+ "hub.verify_token"?: string;
74
+ "hub.challenge"?: string;
75
+ },
76
+ verifyToken: string
77
+ ): string | null {
78
+ return verifyWebhook(query, verifyToken);
79
+ }
80
+
81
+ /**
82
+ * Extract all incoming messages from webhook payload
83
+ *
84
+ * Low-level utility that flattens the nested webhook structure
85
+ * and returns messages directly.
86
+ *
87
+ * @param payload - Webhook payload from Meta
88
+ * @returns Flat array of incoming messages
89
+ */
90
+ extractMessages(payload: WebhookPayload): IncomingMessage[] {
91
+ return extractMessages(payload);
92
+ }
93
+
94
+ /**
95
+ * Extract status updates from webhook payload
96
+ *
97
+ * Low-level utility for extracting status updates for outgoing messages.
98
+ *
99
+ * @param payload - Webhook payload from Meta
100
+ * @returns Flat array of status updates
101
+ */
102
+ extractStatuses(payload: WebhookPayload): unknown[] {
103
+ return extractStatuses(payload);
104
+ }
105
+
106
+ /**
107
+ * Validate webhook payload structure
108
+ *
109
+ * Validates the payload against the schema. Logs errors if malformed
110
+ * but doesn't throw, allowing processing to continue.
111
+ *
112
+ * @param payload - Raw payload to validate
113
+ * @returns Validated payload if valid, original payload if invalid (with logged error)
114
+ */
115
+ private validatePayload(payload: unknown): WebhookPayload {
116
+ const result = webhookPayloadSchema.safeParse(payload);
117
+ if (!result.success) {
118
+ console.error(
119
+ "Webhook payload validation failed:",
120
+ result.error.format()
121
+ );
122
+ // Return as-is (TypeScript will treat it as WebhookPayload, but it's actually invalid)
123
+ // This allows processing to continue, but handlers should be defensive
124
+ return payload as WebhookPayload;
125
+ }
126
+ return result.data;
127
+ }
128
+
129
+ /**
130
+ * Handle webhook payload with type-safe callbacks
131
+ *
132
+ * High-level convenience method that extracts messages and dispatches
133
+ * them to appropriate handlers based on message type.
134
+ *
135
+ * **Important**: This method returns quickly to allow fast webhook responses.
136
+ * Handlers are processed asynchronously. If you need to await handler completion,
137
+ * use the low-level `extractMessages()` method instead.
138
+ *
139
+ * @param payload - Webhook payload from Meta (will be validated)
140
+ * @param handlers - Object with handler functions for each message type
141
+ * @param options - Optional error handling configuration
142
+ */
143
+ handle(
144
+ payload: unknown,
145
+ handlers: MessageHandlers,
146
+ options?: HandleOptions
147
+ ): void {
148
+ // Validate payload (logs error if malformed, but continues)
149
+ const validatedPayload = this.validatePayload(payload);
150
+
151
+ // Extract metadata and contacts from payload for context
152
+ for (const entry of validatedPayload.entry) {
153
+ for (const change of entry.changes) {
154
+ if (change.field === "messages" && change.value.messages) {
155
+ const metadata = {
156
+ phoneNumberId: change.value.metadata.phone_number_id,
157
+ displayPhoneNumber: change.value.metadata.display_phone_number,
158
+ wabaId: entry.id,
159
+ };
160
+
161
+ const contacts = change.value.contacts || [];
162
+
163
+ // Process each message with its context
164
+ for (const message of change.value.messages) {
165
+ // Find contact for this message (match by wa_id)
166
+ const contact = contacts.find((c) => c.wa_id === message.from);
167
+
168
+ // Build context (metadata + contact, no message duplication)
169
+ const context: MessageContext = {
170
+ metadata,
171
+ ...(contact && {
172
+ contact: {
173
+ name: contact.profile.name,
174
+ waId: contact.wa_id,
175
+ },
176
+ }),
177
+ };
178
+
179
+ // Process handler asynchronously (don't await)
180
+ // This allows long-running handlers without blocking webhook response
181
+ Promise.resolve()
182
+ .then(async () => {
183
+ // Type-safe dispatch based on message type
184
+ switch (message.type) {
185
+ case "text":
186
+ if (handlers.text) {
187
+ await handlers.text(message, context);
188
+ }
189
+ break;
190
+
191
+ // Future: image, audio, video, etc.
192
+ default:
193
+ // Unhandled message type - silently continue
194
+ break;
195
+ }
196
+ })
197
+ .catch((error) => {
198
+ // Handle errors in handler execution
199
+ if (options?.onError) {
200
+ options.onError(error as Error, message);
201
+ } else {
202
+ // Default: log and continue (don't break webhook response)
203
+ console.error(
204
+ `Error handling ${message.type} message ${message.id}:`,
205
+ error
206
+ );
207
+ }
208
+ });
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./WebhooksService";
2
+ export type { MessageContext, MessageHandlers, HandleOptions } from "./WebhooksService";
3
+
@@ -0,0 +1,25 @@
1
+ import type { WebhookPayload } from "../../../types/webhooks";
2
+ import type { IncomingMessage } from "../../../types/webhooks/incoming-message";
3
+
4
+ /**
5
+ * Extract all incoming messages from webhook payload
6
+ *
7
+ * Flattens the nested structure: entry[].changes[].value.messages[]
8
+ * Returns a flat array of messages directly
9
+ *
10
+ * @param payload - Webhook payload from Meta
11
+ * @returns Flat array of incoming messages
12
+ */
13
+ export function extractMessages(payload: WebhookPayload): IncomingMessage[] {
14
+ const messages: IncomingMessage[] = [];
15
+
16
+ for (const entry of payload.entry) {
17
+ for (const change of entry.changes) {
18
+ if (change.field === "messages" && change.value.messages) {
19
+ messages.push(...change.value.messages);
20
+ }
21
+ }
22
+ }
23
+
24
+ return messages;
25
+ }