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
@@ -44,7 +44,7 @@ export const clientConfigSchema = z.object({
44
44
  .refine((val) => val === undefined || val.trim().length > 0, {
45
45
  message: "businessId cannot be empty or whitespace only",
46
46
  }),
47
- apiVersion: z.string().default("v18.0"),
48
- baseURL: z.string().url().default("https://graph.facebook.com"),
47
+ apiVersion: z.string().default("v18.0").optional(),
48
+ baseURL: z.string().url().default("https://graph.facebook.com").optional(),
49
49
  timeout: z.number().positive().optional(),
50
50
  });
@@ -2,4 +2,6 @@ export * from "./client";
2
2
  export * from "./messages/index";
3
3
  export * from "./accounts/index";
4
4
  export * from "./business/index";
5
+ export * from "./templates/index";
6
+ export * from "./webhooks/index";
5
7
  export * from "./debug";
@@ -0,0 +1,145 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Button schemas for template components
5
+ * Simplified version without variables for now
6
+ */
7
+
8
+ /**
9
+ * Quick reply button schema
10
+ */
11
+ export const quickReplyButtonSchema = z.object({
12
+ type: z.literal("QUICK_REPLY"),
13
+ text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
14
+ });
15
+
16
+ /**
17
+ * URL button schema
18
+ * Note: example field will be added later when we support variables
19
+ */
20
+ export const urlButtonSchema = z.object({
21
+ type: z.literal("URL"),
22
+ text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
23
+ url: z.string().url().max(2000, "URL must be 2000 characters or less"),
24
+ // example: z.array(z.string()).optional(), // For later: when URL contains variables
25
+ });
26
+
27
+ /**
28
+ * Phone number button schema
29
+ */
30
+ export const phoneNumberButtonSchema = z.object({
31
+ type: z.literal("PHONE_NUMBER"),
32
+ text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
33
+ phone_number: z
34
+ .string()
35
+ .min(1)
36
+ .max(20, "Phone number must be 20 characters or less"),
37
+ });
38
+
39
+ /**
40
+ * Copy code button schema
41
+ * Note: example field will be added later
42
+ */
43
+ export const copyCodeButtonSchema = z.object({
44
+ type: z.literal("COPY_CODE"),
45
+ // example: z.string().max(15).optional(), // For later: example value to copy
46
+ });
47
+
48
+ /**
49
+ * Flow button schema (for authentication templates)
50
+ * Note: Will be expanded later when we support flow templates
51
+ */
52
+ export const flowButtonSchema = z.object({
53
+ type: z.literal("FLOW"),
54
+ text: z.string().min(1).max(25, "Button text must be 25 characters or less"),
55
+ flow_action: z.string().optional(),
56
+ flow_id: z.string().optional(),
57
+ navigate_screen: z.string().optional(),
58
+ });
59
+
60
+ /**
61
+ * Union of all button types
62
+ */
63
+ export const buttonSchema = z.discriminatedUnion("type", [
64
+ quickReplyButtonSchema,
65
+ urlButtonSchema,
66
+ phoneNumberButtonSchema,
67
+ copyCodeButtonSchema,
68
+ flowButtonSchema,
69
+ ]);
70
+
71
+ /**
72
+ * Header component schema
73
+ * Simplified - no variables/examples for now
74
+ *
75
+ * Note:
76
+ * - TEXT format requires text field
77
+ * - IMAGE/VIDEO/DOCUMENT formats require example.header_handle (for later)
78
+ * - LOCATION format requires neither text nor example
79
+ */
80
+ export const headerComponentSchema = z
81
+ .object({
82
+ type: z.literal("HEADER"),
83
+ format: z.enum(["TEXT", "IMAGE", "VIDEO", "DOCUMENT", "LOCATION"]),
84
+ text: z
85
+ .string()
86
+ .max(60, "Header text must be 60 characters or less")
87
+ .optional(),
88
+ // example: z.object({...}).optional(), // For later: when using variables or media
89
+ })
90
+ .refine(
91
+ (data) => {
92
+ // TEXT format requires text
93
+ if (data.format === "TEXT" && !data.text) {
94
+ return false;
95
+ }
96
+ // LOCATION format doesn't need text
97
+ if (data.format === "LOCATION") {
98
+ return true;
99
+ }
100
+ // IMAGE/VIDEO/DOCUMENT will need example.header_handle (for later)
101
+ return true;
102
+ },
103
+ {
104
+ message: "TEXT format header requires text field",
105
+ }
106
+ );
107
+
108
+ /**
109
+ * Body component schema
110
+ * Required component - no variables for now
111
+ */
112
+ export const bodyComponentSchema = z.object({
113
+ type: z.literal("BODY"),
114
+ text: z
115
+ .string()
116
+ .min(1)
117
+ .max(1024, "Body text must be 1024 characters or less"),
118
+ // example: z.object({...}).optional(), // For later: when using variables
119
+ });
120
+
121
+ /**
122
+ * Footer component schema
123
+ */
124
+ export const footerComponentSchema = z.object({
125
+ type: z.literal("FOOTER"),
126
+ text: z.string().min(1).max(60, "Footer text must be 60 characters or less"),
127
+ });
128
+
129
+ /**
130
+ * Buttons component schema
131
+ */
132
+ export const buttonsComponentSchema = z.object({
133
+ type: z.literal("BUTTONS"),
134
+ buttons: z.array(buttonSchema).min(1).max(10, "Maximum 10 buttons allowed"),
135
+ });
136
+
137
+ /**
138
+ * Union of all component types
139
+ */
140
+ export const componentSchema = z.discriminatedUnion("type", [
141
+ headerComponentSchema,
142
+ bodyComponentSchema,
143
+ footerComponentSchema,
144
+ buttonsComponentSchema,
145
+ ]);
@@ -0,0 +1,4 @@
1
+ export * from "./component";
2
+ export * from "./request";
3
+ export * from "./response";
4
+
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { componentSchema } from "./component";
3
+
4
+ /**
5
+ * Schema for creating a template
6
+ * Simplified - no variables/examples for now
7
+ */
8
+ export const createTemplateRequestSchema = z.object({
9
+ name: z.string().min(1).max(512, "Template name must be 512 characters or less"),
10
+ language: z.string().min(2).max(5, "Language code must be 2-5 characters (e.g., 'en' or 'en_US')"),
11
+ category: z.enum(["AUTHENTICATION", "MARKETING", "UTILITY"]),
12
+ components: z
13
+ .array(componentSchema)
14
+ .min(1, "At least one component is required")
15
+ .refine(
16
+ (components) => {
17
+ // Body component is required
18
+ return components.some((c) => c.type === "BODY");
19
+ },
20
+ { message: "BODY component is required" }
21
+ )
22
+ .refine(
23
+ (components) => {
24
+ // Only one header allowed
25
+ const headers = components.filter((c) => c.type === "HEADER");
26
+ return headers.length <= 1;
27
+ },
28
+ { message: "Only one HEADER component is allowed" }
29
+ )
30
+ .refine(
31
+ (components) => {
32
+ // Only one footer allowed
33
+ const footers = components.filter((c) => c.type === "FOOTER");
34
+ return footers.length <= 1;
35
+ },
36
+ { message: "Only one FOOTER component is allowed" }
37
+ )
38
+ .refine(
39
+ (components) => {
40
+ // Only one buttons component allowed
41
+ const buttons = components.filter((c) => c.type === "BUTTONS");
42
+ return buttons.length <= 1;
43
+ },
44
+ { message: "Only one BUTTONS component is allowed" }
45
+ ),
46
+ });
47
+
48
+ /**
49
+ * Schema for updating a template
50
+ * All fields optional - only update what's provided
51
+ */
52
+ export const updateTemplateRequestSchema = z.object({
53
+ category: z.enum(["AUTHENTICATION", "MARKETING", "UTILITY"]).optional(),
54
+ components: z.array(componentSchema).optional(),
55
+ language: z.string().min(2).max(5).optional(),
56
+ name: z.string().min(1).max(512).optional(),
57
+ });
58
+
59
+ /**
60
+ * Schema for listing templates
61
+ */
62
+ export const listTemplatesRequestSchema = z.object({
63
+ name: z.string().optional(), // Filter by template name
64
+ });
65
+
66
+ /**
67
+ * Schema for deleting a template
68
+ * Either name or hsm_id must be provided
69
+ */
70
+ export const deleteTemplateRequestSchema = z
71
+ .object({
72
+ name: z.string().optional(),
73
+ hsm_id: z.string().optional(),
74
+ })
75
+ .refine((data) => data.name || data.hsm_id, {
76
+ message: "Either name or hsm_id must be provided",
77
+ });
78
+
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ import { componentSchema } from "./component";
3
+
4
+ /**
5
+ * Schema for template response (single template)
6
+ */
7
+ export const templateResponseSchema = z.object({
8
+ id: z.string(),
9
+ name: z.string(),
10
+ language: z.string(),
11
+ status: z.string(),
12
+ category: z.string(),
13
+ components: z.array(componentSchema),
14
+ });
15
+
16
+ /**
17
+ * Schema for create template response
18
+ */
19
+ export const createTemplateResponseSchema = z.object({
20
+ id: z.string(),
21
+ status: z.string(),
22
+ category: z.string(),
23
+ });
24
+
25
+ /**
26
+ * Schema for list templates response
27
+ */
28
+ export const templateListItemSchema = z.object({
29
+ id: z.string(),
30
+ name: z.string(),
31
+ language: z.string(),
32
+ status: z.string(),
33
+ category: z.string(),
34
+ components: z.array(componentSchema),
35
+ });
36
+
37
+ export const listTemplatesResponseSchema = z.object({
38
+ data: z.array(templateListItemSchema),
39
+ paging: z
40
+ .object({
41
+ cursors: z
42
+ .object({
43
+ before: z.string().optional(),
44
+ after: z.string().optional(),
45
+ })
46
+ .optional(),
47
+ })
48
+ .optional(),
49
+ });
50
+
51
+ /**
52
+ * Schema for update template response
53
+ */
54
+ export const updateTemplateResponseSchema = z.object({
55
+ success: z.boolean(),
56
+ });
57
+
58
+ /**
59
+ * Schema for delete template response
60
+ */
61
+ export const deleteTemplateResponseSchema = z.object({
62
+ success: z.boolean(),
63
+ });
64
+
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Base fields present in ALL incoming messages
5
+ */
6
+ const baseIncomingMessageSchema = z.object({
7
+ from: z.string(), // WhatsApp ID (phone number without +)
8
+ id: z.string(), // Message ID (wamid.*)
9
+ timestamp: z.string(), // Unix timestamp as string
10
+ type: z.string(), // Message type discriminator
11
+ });
12
+
13
+ /**
14
+ * Text content in incoming text messages
15
+ * Note: Incoming messages don't have preview_url like outgoing
16
+ */
17
+ const incomingTextContentSchema = z.object({
18
+ body: z.string(),
19
+ });
20
+
21
+ /**
22
+ * Incoming text message schema
23
+ * Uses discriminated union pattern (type: "text")
24
+ */
25
+ export const incomingTextMessageSchema = baseIncomingMessageSchema.extend({
26
+ type: z.literal("text"),
27
+ text: incomingTextContentSchema,
28
+ });
29
+
30
+ /**
31
+ * Union of all incoming message types
32
+ * For now: just text. Others (image, audio, etc.) will be added later
33
+ */
34
+ export const incomingMessageSchema = z.discriminatedUnion("type", [
35
+ incomingTextMessageSchema,
36
+ // Future: incomingImageMessageSchema, incomingAudioMessageSchema, etc.
37
+ ]);
38
+
@@ -0,0 +1,3 @@
1
+ export * from "./incoming-message";
2
+ export * from "./payload";
3
+
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { incomingMessageSchema } from "./incoming-message";
3
+
4
+ /**
5
+ * Contact information in webhook
6
+ */
7
+ const contactSchema = z.object({
8
+ profile: z.object({
9
+ name: z.string(),
10
+ }),
11
+ wa_id: z.string(),
12
+ });
13
+
14
+ /**
15
+ * Metadata in webhook value
16
+ */
17
+ const webhookMetadataSchema = z.object({
18
+ display_phone_number: z.string(),
19
+ phone_number_id: z.string(),
20
+ });
21
+
22
+ /**
23
+ * Webhook value (the actual data)
24
+ */
25
+ const webhookValueSchema = z.object({
26
+ messaging_product: z.literal("whatsapp"),
27
+ metadata: webhookMetadataSchema,
28
+ contacts: z.array(contactSchema).optional(),
29
+ messages: z.array(incomingMessageSchema).optional(), // Incoming messages
30
+ statuses: z.array(z.any()).optional(), // Status updates (for later)
31
+ });
32
+
33
+ /**
34
+ * Webhook change entry
35
+ */
36
+ const webhookChangeSchema = z.object({
37
+ value: webhookValueSchema,
38
+ field: z.literal("messages"), // For now: only messages field
39
+ });
40
+
41
+ /**
42
+ * Webhook entry
43
+ */
44
+ const webhookEntrySchema = z.object({
45
+ id: z.string(), // WABA ID
46
+ changes: z.array(webhookChangeSchema),
47
+ });
48
+
49
+ /**
50
+ * Full webhook payload schema
51
+ */
52
+ export const webhookPayloadSchema = z.object({
53
+ object: z.literal("whatsapp_business_account"),
54
+ entry: z.array(webhookEntrySchema),
55
+ });
56
+
@@ -1,42 +1,34 @@
1
1
  import type { HttpClient } from "../../client/HttpClient";
2
2
 
3
3
  /**
4
- * Accounts client - wraps HttpClient with WABA ID (WhatsApp Business Account ID) as base endpoint
4
+ * Accounts client - wraps HttpClient with WABA ID as base endpoint
5
5
  *
6
- * This client automatically prepends `/${wabaId}` to all request paths,
7
- * so methods can use relative paths like `/phone_numbers` instead of `/${wabaId}/phone_numbers`.
8
- *
9
- * Note: The wabaId is the WhatsApp Business Account ID (not the Business Portfolio ID).
10
- * This is used in endpoints like GET /<WABA_ID>/phone_numbers.
11
- *
12
- * This treats wabaId as a "client" for the accounts namespace - different
13
- * wabaIds represent different WhatsApp Business Account endpoints.
6
+ * This client automatically prepends `/${businessAccountId}` to all request paths.
14
7
  */
15
8
  export class AccountsClient {
16
9
  constructor(
17
10
  private readonly httpClient: HttpClient,
18
- private readonly wabaId: string
11
+ private readonly businessAccountId: string
19
12
  ) {}
20
13
 
21
14
  /**
22
15
  * Make a GET request with WABA ID prefix
23
16
  */
24
17
  async get<T>(path: string): Promise<T> {
25
- return this.httpClient.get<T>(`/${this.wabaId}${path}`);
18
+ return this.httpClient.get<T>(`/${this.businessAccountId}${path}`);
26
19
  }
27
20
 
28
21
  /**
29
22
  * Make a POST request with WABA ID prefix
30
23
  */
31
24
  async post<T>(path: string, body: unknown): Promise<T> {
32
- return this.httpClient.post<T>(`/${this.wabaId}${path}`, body);
25
+ return this.httpClient.post<T>(`/${this.businessAccountId}${path}`, body);
33
26
  }
34
27
 
35
28
  /**
36
29
  * Make a PATCH request with WABA ID prefix
37
30
  */
38
31
  async patch<T>(path: string, body: unknown): Promise<T> {
39
- return this.httpClient.patch<T>(`/${this.wabaId}${path}`, body);
32
+ return this.httpClient.patch<T>(`/${this.businessAccountId}${path}`, body);
40
33
  }
41
34
  }
42
-
@@ -7,41 +7,39 @@ import type { PhoneNumberListResponse } from "../../types/accounts/phone-number"
7
7
  /**
8
8
  * Accounts service for managing WhatsApp Business Accounts
9
9
  *
10
- * The service validates that businessAccountId (WABA ID - WhatsApp Business Account ID) is set
11
- * at the client level and creates an AccountsClient instance.
12
- *
13
- * Note: businessAccountId in the client config represents the WABA ID, not the Business Portfolio ID.
14
- * The WABA ID is used in endpoints like GET /<WABA_ID>/phone_numbers.
15
- *
16
- * AccountsClient treats wabaId as a "client" for the accounts namespace - different
17
- * wabaIds represent different WhatsApp Business Account endpoints.
10
+ * This service handles WABA operations like listing phone numbers.
11
+ * It supports both a globally configured businessAccountId (in WhatsAppClient)
12
+ * and per-request businessAccountId overrides.
18
13
  */
19
14
  export class AccountsService {
20
- private readonly accountsClient: AccountsClient;
15
+ constructor(private readonly httpClient: HttpClient) {}
21
16
 
22
- constructor(httpClient: HttpClient) {
23
- // Validate that businessAccountId (WABA ID) is set at client level
24
- // This is the WhatsApp Business Account ID, not the Business Portfolio ID
25
- if (!httpClient.businessAccountId) {
17
+ /**
18
+ * Helper to create a Scoped Client (prefer override, fallback to config)
19
+ */
20
+ private getClient(overrideId?: string): AccountsClient {
21
+ const id = overrideId || this.httpClient.businessAccountId;
22
+ if (!id) {
26
23
  throw new WhatsAppValidationError(
27
- "businessAccountId (WABA ID - WhatsApp Business Account ID) is required for AccountsService. Provide it in WhatsAppClient config.",
24
+ "businessAccountId (WABA ID) is required. Provide it in WhatsAppClient config or as a parameter.",
28
25
  "businessAccountId"
29
26
  );
30
27
  }
31
28
 
32
- // Create accounts client with WABA ID baked in
33
- this.accountsClient = new AccountsClient(
34
- httpClient,
35
- httpClient.businessAccountId
36
- );
29
+ // Just wrap the existing httpClient
30
+ return new AccountsClient(this.httpClient, id);
37
31
  }
38
32
 
39
33
  /**
40
34
  * List phone numbers for a WhatsApp Business Account
41
35
  *
36
+ * @param businessAccountId - Optional WABA ID (overrides client config)
42
37
  * @returns List of phone numbers associated with the WABA
43
38
  */
44
- async listPhoneNumbers(): Promise<PhoneNumberListResponse> {
45
- return listPhoneNumbers(this.accountsClient);
39
+ async listPhoneNumbers(
40
+ businessAccountId?: string
41
+ ): Promise<PhoneNumberListResponse> {
42
+ const client = this.getClient(businessAccountId);
43
+ return listPhoneNumbers(client);
46
44
  }
47
45
  }
@@ -4,7 +4,7 @@ import type { PhoneNumberListResponse } from "../../../types/accounts/phone-numb
4
4
  /**
5
5
  * List phone numbers for a WhatsApp Business Account
6
6
  *
7
- * @param accountsClient - Accounts client with WABA ID baked in
7
+ * @param accountsClient - Scoped accounts client
8
8
  * @returns List of phone numbers associated with the WABA
9
9
  */
10
10
  export async function listPhoneNumbers(
@@ -13,4 +13,3 @@ export async function listPhoneNumbers(
13
13
  // Make API request - accountsClient handles the WABA ID prefix automatically
14
14
  return accountsClient.get<PhoneNumberListResponse>("/phone_numbers");
15
15
  }
16
-
@@ -3,14 +3,7 @@ import type { HttpClient } from "../../client/HttpClient";
3
3
  /**
4
4
  * Business client - wraps HttpClient with Business Portfolio ID as base endpoint
5
5
  *
6
- * This client automatically prepends `/${businessId}` to all request paths,
7
- * so methods can use relative paths like `/whatsapp_business_accounts` instead of `/${businessId}/whatsapp_business_accounts`.
8
- *
9
- * Note: The businessId is the Business Portfolio ID (not the WABA ID).
10
- * This is used in endpoints like GET /<Business-ID>/whatsapp_business_accounts.
11
- *
12
- * This treats businessId as a "client" for the business namespace - different
13
- * businessIds represent different Business Portfolio endpoints.
6
+ * This client automatically prepends `/${businessId}` to all request paths.
14
7
  */
15
8
  export class BusinessClient {
16
9
  constructor(
@@ -39,4 +32,3 @@ export class BusinessClient {
39
32
  return this.httpClient.patch<T>(`/${this.businessId}${path}`, body);
40
33
  }
41
34
  }
42
-
@@ -7,41 +7,39 @@ import type { BusinessAccountsListResponse } from "../../types/business/account"
7
7
  /**
8
8
  * Business service for managing Business Portfolios
9
9
  *
10
- * The service validates that businessId (Business Portfolio ID) is set at the client level
11
- * and creates a BusinessClient instance.
12
- *
13
- * Note: businessId in the client config represents the Business Portfolio ID.
14
- * The Business Portfolio ID is used in endpoints like GET /<Business-ID>/whatsapp_business_accounts.
15
- *
16
- * BusinessClient treats businessId as a "client" for the business namespace - different
17
- * businessIds represent different Business Portfolio endpoints.
10
+ * This service handles Business Portfolio operations like listing WABAs.
11
+ * It supports both a globally configured businessId (in WhatsAppClient)
12
+ * and per-request businessId overrides.
18
13
  */
19
14
  export class BusinessService {
20
- private readonly businessClient: BusinessClient;
15
+ constructor(private readonly httpClient: HttpClient) {}
21
16
 
22
- constructor(httpClient: HttpClient) {
23
- // Validate that businessId (Business Portfolio ID) is set at client level
24
- if (!httpClient.businessId) {
17
+ /**
18
+ * Helper to create a Scoped Client (prefer override, fallback to config)
19
+ */
20
+ private getClient(overrideId?: string): BusinessClient {
21
+ const id = overrideId || this.httpClient.businessId;
22
+ if (!id) {
25
23
  throw new WhatsAppValidationError(
26
- "businessId (Business Portfolio ID) is required for BusinessService. Provide it in WhatsAppClient config.",
24
+ "businessId (Business Portfolio ID) is required. Provide it in WhatsAppClient config or as a parameter.",
27
25
  "businessId"
28
26
  );
29
27
  }
30
28
 
31
- // Create business client with Business Portfolio ID baked in
32
- this.businessClient = new BusinessClient(
33
- httpClient,
34
- httpClient.businessId
35
- );
29
+ // Just wrap the existing httpClient
30
+ return new BusinessClient(this.httpClient, id);
36
31
  }
37
32
 
38
33
  /**
39
34
  * List WhatsApp Business Accounts (WABAs) for a Business Portfolio
40
35
  *
36
+ * @param businessId - Optional Business Portfolio ID (overrides client config)
41
37
  * @returns List of WABAs associated with the Business Portfolio
42
38
  */
43
- async listAccounts(): Promise<BusinessAccountsListResponse> {
44
- return listAccounts(this.businessClient);
39
+ async listAccounts(
40
+ businessId?: string
41
+ ): Promise<BusinessAccountsListResponse> {
42
+ const client = this.getClient(businessId);
43
+ return listAccounts(client);
45
44
  }
46
45
  }
47
-
@@ -4,7 +4,7 @@ import type { BusinessAccountsListResponse } from "../../../types/business/accou
4
4
  /**
5
5
  * List WhatsApp Business Accounts (WABAs) for a Business Portfolio
6
6
  *
7
- * @param businessClient - Business client with Business Portfolio ID baked in
7
+ * @param businessClient - Scoped business client
8
8
  * @returns List of WABAs associated with the Business Portfolio
9
9
  */
10
10
  export async function listAccounts(
@@ -15,4 +15,3 @@ export async function listAccounts(
15
15
  "/whatsapp_business_accounts"
16
16
  );
17
17
  }
18
-
@@ -2,12 +2,8 @@ import type { HttpClient } from "../../client/HttpClient";
2
2
 
3
3
  /**
4
4
  * Messages client - wraps HttpClient with phone number ID as base endpoint
5
- *
6
- * This client automatically prepends `/${phoneNumberId}` to all request paths,
7
- * so methods can use relative paths like `/messages` instead of `/${phoneNumberId}/messages`.
8
- *
9
- * This treats phoneNumberId as a "client" for the messaging namespace - different
10
- * phoneNumberIds represent different messaging endpoints.
5
+ *
6
+ * This client automatically prepends `/${phoneNumberId}` to all request paths.
11
7
  */
12
8
  export class MessagesClient {
13
9
  constructor(