whatsapp-cloud 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +11 -0
- package/agent_docs/DESIGN.md +707 -0
- package/agent_docs/MESSAGES_NAMESPACE_ANALYSIS.md +368 -0
- package/agent_docs/NAMING_DECISION.md +78 -0
- package/agent_docs/STRUCTURE.md +711 -0
- package/agent_docs/messages-namespace-design.md +357 -0
- package/package.json +5 -2
- package/src/client/HttpClient.ts +122 -0
- package/src/client/WhatsAppClient.ts +55 -0
- package/src/client/index.ts +2 -0
- package/src/errors.ts +58 -0
- package/src/index.ts +16 -1
- package/src/schemas/accounts/index.ts +1 -0
- package/src/schemas/accounts/phone-number.ts +20 -0
- package/src/schemas/business/account.ts +43 -0
- package/src/schemas/business/index.ts +2 -0
- package/src/schemas/client.ts +50 -0
- package/src/schemas/debug.ts +25 -0
- package/src/schemas/index.ts +5 -0
- package/src/schemas/messages/index.ts +2 -0
- package/src/schemas/messages/request.ts +82 -0
- package/src/schemas/messages/response.ts +19 -0
- package/src/services/accounts/AccountsClient.ts +42 -0
- package/src/services/accounts/AccountsService.ts +47 -0
- package/src/services/accounts/index.ts +2 -0
- package/src/services/accounts/methods/list-phone-numbers.ts +16 -0
- package/src/services/business/BusinessClient.ts +42 -0
- package/src/services/business/BusinessService.ts +47 -0
- package/src/services/business/index.ts +3 -0
- package/src/services/business/methods/list-accounts.ts +18 -0
- package/src/services/index.ts +2 -0
- package/src/services/messages/MessagesClient.ts +38 -0
- package/src/services/messages/MessagesService.ts +77 -0
- package/src/services/messages/index.ts +8 -0
- package/src/services/messages/methods/send-image.ts +33 -0
- package/src/services/messages/methods/send-location.ts +32 -0
- package/src/services/messages/methods/send-reaction.ts +33 -0
- package/src/services/messages/methods/send-text.ts +32 -0
- package/src/services/messages/utils/build-message-payload.ts +32 -0
- package/src/types/accounts/index.ts +1 -0
- package/src/types/accounts/phone-number.ts +9 -0
- package/src/types/business/account.ts +10 -0
- package/src/types/business/index.ts +2 -0
- package/src/types/client.ts +8 -0
- package/src/types/debug.ts +8 -0
- package/src/types/index.ts +5 -0
- package/src/types/messages/index.ts +2 -0
- package/src/types/messages/request.ts +27 -0
- package/src/types/messages/response.ts +7 -0
- package/src/utils/zod-error.ts +28 -0
- package/tsconfig.json +4 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Messages Namespace Design
|
|
2
|
+
|
|
3
|
+
This document describes the design patterns and architectural decisions for the Messages namespace. This serves as a blueprint for implementing other namespaces in the WhatsApp Cloud API SDK.
|
|
4
|
+
|
|
5
|
+
## Core Design Principles
|
|
6
|
+
|
|
7
|
+
### 1. **Namespace Client Pattern**
|
|
8
|
+
|
|
9
|
+
Each namespace that requires a base path identifier (like `phoneNumberId` for messages) gets its own client wrapper that handles the base path automatically.
|
|
10
|
+
|
|
11
|
+
**Pattern:**
|
|
12
|
+
- Create a `{Namespace}Client` class that wraps `HttpClient`
|
|
13
|
+
- The client automatically prepends the namespace identifier to all paths
|
|
14
|
+
- Methods use simple relative paths (e.g., `/messages`) instead of full paths (e.g., `/${phoneNumberId}/messages`)
|
|
15
|
+
|
|
16
|
+
**Example:**
|
|
17
|
+
```typescript
|
|
18
|
+
// MessagesClient wraps HttpClient with phoneNumberId as base endpoint
|
|
19
|
+
export class MessagesClient {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly httpClient: HttpClient,
|
|
22
|
+
private readonly phoneNumberId: string
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
async post<T>(path: string, body: unknown): Promise<T> {
|
|
26
|
+
return this.httpClient.post<T>(`/${this.phoneNumberId}${path}`, body);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Why:** Treats the namespace identifier (phoneNumberId) as a "client" for that namespace. Different identifiers represent different endpoints, making the abstraction clean and intuitive.
|
|
32
|
+
|
|
33
|
+
### 2. **Request Structure Matches API**
|
|
34
|
+
|
|
35
|
+
Request schemas match the WhatsApp API structure exactly, minus fields that are handled internally.
|
|
36
|
+
|
|
37
|
+
**Pattern:**
|
|
38
|
+
- Request schemas mirror the API payload structure
|
|
39
|
+
- Exclude: `messaging_product`, `recipient_type`, `type` (added by `buildMessagePayload`)
|
|
40
|
+
- Exclude: `phoneNumberId` (handled at client level)
|
|
41
|
+
- Include: All user-provided fields matching API structure
|
|
42
|
+
|
|
43
|
+
**Example:**
|
|
44
|
+
```typescript
|
|
45
|
+
// API expects: { to, image: { id?, link?, caption? } }
|
|
46
|
+
// Schema matches: { to, image: { id?, link?, caption? } }
|
|
47
|
+
export const sendImageRequestSchema = baseMessageRequestSchema.extend({
|
|
48
|
+
image: imageSchema, // Matches API structure
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Why:**
|
|
53
|
+
- Reduces transformation logic
|
|
54
|
+
- Makes API documentation directly applicable
|
|
55
|
+
- Clear mapping between user input and API payload
|
|
56
|
+
|
|
57
|
+
### 3. **Client-Level Configuration**
|
|
58
|
+
|
|
59
|
+
Namespace-specific identifiers (like `phoneNumberId`) are handled at the service construction level, not in individual requests.
|
|
60
|
+
|
|
61
|
+
**Pattern:**
|
|
62
|
+
- Service validates required identifier exists in `HttpClient` at construction
|
|
63
|
+
- Service creates namespace client once with the identifier
|
|
64
|
+
- Methods receive the namespace client, not the raw `HttpClient`
|
|
65
|
+
- No per-request identifier resolution needed
|
|
66
|
+
|
|
67
|
+
**Example:**
|
|
68
|
+
```typescript
|
|
69
|
+
export class MessagesService {
|
|
70
|
+
private readonly messagesClient: MessagesClient;
|
|
71
|
+
|
|
72
|
+
constructor(httpClient: HttpClient) {
|
|
73
|
+
// Validate identifier at construction
|
|
74
|
+
if (!httpClient.phoneNumberId) {
|
|
75
|
+
throw new WhatsAppValidationError(...);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create namespace client once
|
|
79
|
+
this.messagesClient = new MessagesClient(httpClient, httpClient.phoneNumberId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async sendImage(request: SendImageRequest) {
|
|
83
|
+
// Method receives namespace client, not HttpClient
|
|
84
|
+
return sendImage(this.messagesClient, request);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Why:**
|
|
90
|
+
- Identifier is required, so validate early (fail fast)
|
|
91
|
+
- Single source of truth for namespace identifier
|
|
92
|
+
- Methods don't need to handle optional overrides
|
|
93
|
+
- Cleaner method signatures
|
|
94
|
+
|
|
95
|
+
### 4. **Clean Method Pattern**
|
|
96
|
+
|
|
97
|
+
All methods follow a consistent pattern: validate → extract → build → request.
|
|
98
|
+
|
|
99
|
+
**Pattern:**
|
|
100
|
+
```typescript
|
|
101
|
+
export async function sendImage(
|
|
102
|
+
messagesClient: MessagesClient,
|
|
103
|
+
request: SendImageRequest
|
|
104
|
+
): Promise<MessageResponse> {
|
|
105
|
+
// 1. Validate request with schema
|
|
106
|
+
const result = sendImageRequestSchema.safeParse(request);
|
|
107
|
+
if (!result.success) {
|
|
108
|
+
throw transformZodError(result.error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Extract validated data
|
|
112
|
+
const data = result.data;
|
|
113
|
+
|
|
114
|
+
// 3. Build payload (request structure already matches API)
|
|
115
|
+
const payload = buildMessagePayload(data.to, "image", {
|
|
116
|
+
image: data.image,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 4. Make API request (namespace client handles base path)
|
|
120
|
+
return messagesClient.post<MessageResponse>("/messages", payload);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Why:**
|
|
125
|
+
- Consistent, predictable flow
|
|
126
|
+
- Easy to understand and maintain
|
|
127
|
+
- Clear separation of concerns
|
|
128
|
+
- Request → result → data trajectory
|
|
129
|
+
|
|
130
|
+
### 5. **Request → Result → Data Trajectory**
|
|
131
|
+
|
|
132
|
+
The validation flow follows a clear trajectory: request → validation result → validated data.
|
|
133
|
+
|
|
134
|
+
**Pattern:**
|
|
135
|
+
```typescript
|
|
136
|
+
// Request comes in
|
|
137
|
+
const result = schema.safeParse(request);
|
|
138
|
+
|
|
139
|
+
// Extract validated data
|
|
140
|
+
const data = result.data;
|
|
141
|
+
|
|
142
|
+
// Use data directly (structure matches API)
|
|
143
|
+
const payload = buildMessagePayload(data.to, "image", {
|
|
144
|
+
image: data.image,
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Why:**
|
|
149
|
+
- Clear path from user input to safe API request
|
|
150
|
+
- No intermediate transformations needed
|
|
151
|
+
- Type-safe throughout the flow
|
|
152
|
+
|
|
153
|
+
### 6. **No Manual Undefined Filtering**
|
|
154
|
+
|
|
155
|
+
Let `JSON.stringify` handle undefined values automatically.
|
|
156
|
+
|
|
157
|
+
**Pattern:**
|
|
158
|
+
```typescript
|
|
159
|
+
// Don't filter undefined manually
|
|
160
|
+
export function buildMessagePayload(to: string, type: string, content: T) {
|
|
161
|
+
return {
|
|
162
|
+
messaging_product: "whatsapp" as const,
|
|
163
|
+
recipient_type: "individual" as const,
|
|
164
|
+
to,
|
|
165
|
+
type,
|
|
166
|
+
...content, // undefined values automatically omitted by JSON.stringify
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Why:**
|
|
172
|
+
- `JSON.stringify` automatically omits undefined values
|
|
173
|
+
- Less code, simpler logic
|
|
174
|
+
- No need for `omitUndefined` utilities
|
|
175
|
+
|
|
176
|
+
## Architecture Layers
|
|
177
|
+
|
|
178
|
+
### Layer 1: Service (MessagesService)
|
|
179
|
+
- **Responsibility:** Validate namespace requirements, create namespace client
|
|
180
|
+
- **Input:** `HttpClient` (with namespace identifier)
|
|
181
|
+
- **Output:** Namespace client instance
|
|
182
|
+
- **Pattern:** Constructor validation + client creation
|
|
183
|
+
|
|
184
|
+
### Layer 2: Namespace Client (MessagesClient)
|
|
185
|
+
- **Responsibility:** Handle namespace-specific base path
|
|
186
|
+
- **Input:** `HttpClient` + namespace identifier
|
|
187
|
+
- **Output:** Wrapped client with base path handling
|
|
188
|
+
- **Pattern:** Proxy pattern - wraps HttpClient with path prefix
|
|
189
|
+
|
|
190
|
+
### Layer 3: Methods (sendImage, sendText, etc.)
|
|
191
|
+
- **Responsibility:** Validate request, build payload, make API call
|
|
192
|
+
- **Input:** Namespace client + request object
|
|
193
|
+
- **Output:** API response
|
|
194
|
+
- **Pattern:** Validate → extract → build → request
|
|
195
|
+
|
|
196
|
+
### Layer 4: Utilities (buildMessagePayload)
|
|
197
|
+
- **Responsibility:** Add common API fields
|
|
198
|
+
- **Input:** User data
|
|
199
|
+
- **Output:** Complete API payload
|
|
200
|
+
- **Pattern:** Simple object construction
|
|
201
|
+
|
|
202
|
+
## File Structure
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
services/messages/
|
|
206
|
+
├── MessagesService.ts # Service layer - validates & creates client
|
|
207
|
+
├── MessagesClient.ts # Namespace client - handles base path
|
|
208
|
+
├── methods/
|
|
209
|
+
│ ├── send-image.ts # Method implementations
|
|
210
|
+
│ ├── send-text.ts
|
|
211
|
+
│ ├── send-location.ts
|
|
212
|
+
│ └── send-reaction.ts
|
|
213
|
+
├── utils/
|
|
214
|
+
│ └── build-message-payload.ts # Common payload builder
|
|
215
|
+
└── index.ts # Exports
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Schema Structure
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
schemas/messages/
|
|
222
|
+
├── request.ts # Request schemas matching API structure
|
|
223
|
+
└── response.ts # Response schemas
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Request Schema Pattern:**
|
|
227
|
+
```typescript
|
|
228
|
+
// Base schema (common fields)
|
|
229
|
+
const baseMessageRequestSchema = z.object({
|
|
230
|
+
to: z.string().regex(...),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Type-specific schema (matches API structure)
|
|
234
|
+
const imageSchema = z.object({
|
|
235
|
+
id: z.string().optional(),
|
|
236
|
+
link: z.string().url().optional(),
|
|
237
|
+
caption: z.string().max(1024).optional(),
|
|
238
|
+
}).refine(...);
|
|
239
|
+
|
|
240
|
+
// Combined schema
|
|
241
|
+
export const sendImageRequestSchema = baseMessageRequestSchema.extend({
|
|
242
|
+
image: imageSchema,
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Type Structure
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
types/messages/
|
|
250
|
+
├── request.ts # Request types (inferred from schemas)
|
|
251
|
+
└── response.ts # Response types
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Type Pattern:**
|
|
255
|
+
```typescript
|
|
256
|
+
// Types are inferred from schemas
|
|
257
|
+
export type SendImageRequest = z.infer<typeof sendImageRequestSchema>;
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Key Design Decisions
|
|
261
|
+
|
|
262
|
+
### ✅ Do's
|
|
263
|
+
|
|
264
|
+
1. **Match API structure in requests** - Makes documentation directly applicable
|
|
265
|
+
2. **Validate at service construction** - Fail fast, clear errors
|
|
266
|
+
3. **Use namespace clients** - Clean abstraction for namespace-specific paths
|
|
267
|
+
4. **Follow consistent method pattern** - Easy to understand and maintain
|
|
268
|
+
5. **Let JSON.stringify handle undefined** - Simpler code
|
|
269
|
+
|
|
270
|
+
### ❌ Don'ts
|
|
271
|
+
|
|
272
|
+
1. **Don't include namespace identifiers in requests** - Handle at client level
|
|
273
|
+
2. **Don't manually filter undefined** - Let JSON.stringify do it
|
|
274
|
+
3. **Don't transform request structure unnecessarily** - Match API directly
|
|
275
|
+
4. **Don't resolve identifiers in methods** - Do it at service level
|
|
276
|
+
5. **Don't create namespace client per request** - Create once in constructor
|
|
277
|
+
|
|
278
|
+
## Applying to Other Namespaces
|
|
279
|
+
|
|
280
|
+
When creating a new namespace (e.g., `BusinessAccounts`):
|
|
281
|
+
|
|
282
|
+
1. **Create namespace client** (`BusinessAccountsClient`)
|
|
283
|
+
- Wrap `HttpClient` with namespace-specific base path
|
|
284
|
+
- Handle namespace identifier (e.g., `businessAccountId`)
|
|
285
|
+
|
|
286
|
+
2. **Create service** (`BusinessAccountsService`)
|
|
287
|
+
- Validate namespace identifier exists in constructor
|
|
288
|
+
- Create namespace client once
|
|
289
|
+
- Pass client to methods
|
|
290
|
+
|
|
291
|
+
3. **Create methods** (`getProfile`, `updateProfile`, etc.)
|
|
292
|
+
- Follow validate → extract → build → request pattern
|
|
293
|
+
- Use namespace client, not raw `HttpClient`
|
|
294
|
+
- Match API structure in requests
|
|
295
|
+
|
|
296
|
+
4. **Create schemas** (`schemas/business-accounts/`)
|
|
297
|
+
- Match API structure exactly
|
|
298
|
+
- Exclude internal fields (handled by utilities)
|
|
299
|
+
- Exclude namespace identifier (handled at client level)
|
|
300
|
+
|
|
301
|
+
5. **Create types** (`types/business-accounts/`)
|
|
302
|
+
- Infer from schemas using `z.infer`
|
|
303
|
+
|
|
304
|
+
## Example: Applying Pattern to Business Accounts
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// 1. Namespace Client
|
|
308
|
+
export class BusinessAccountsClient {
|
|
309
|
+
constructor(
|
|
310
|
+
private readonly httpClient: HttpClient,
|
|
311
|
+
private readonly businessAccountId: string
|
|
312
|
+
) {}
|
|
313
|
+
|
|
314
|
+
async get<T>(path: string): Promise<T> {
|
|
315
|
+
return this.httpClient.get<T>(`/${this.businessAccountId}${path}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 2. Service
|
|
320
|
+
export class BusinessAccountsService {
|
|
321
|
+
private readonly accountsClient: BusinessAccountsClient;
|
|
322
|
+
|
|
323
|
+
constructor(httpClient: HttpClient) {
|
|
324
|
+
if (!httpClient.businessAccountId) {
|
|
325
|
+
throw new WhatsAppValidationError(...);
|
|
326
|
+
}
|
|
327
|
+
this.accountsClient = new BusinessAccountsClient(
|
|
328
|
+
httpClient,
|
|
329
|
+
httpClient.businessAccountId
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async getProfile() {
|
|
334
|
+
return getProfile(this.accountsClient);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 3. Method
|
|
339
|
+
export async function getProfile(
|
|
340
|
+
accountsClient: BusinessAccountsClient
|
|
341
|
+
): Promise<ProfileResponse> {
|
|
342
|
+
return accountsClient.get<ProfileResponse>("/profile");
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Summary
|
|
347
|
+
|
|
348
|
+
The Messages namespace design provides a clean, consistent pattern for implementing API namespaces:
|
|
349
|
+
|
|
350
|
+
- **Namespace clients** handle base paths automatically
|
|
351
|
+
- **Request structures** match API directly
|
|
352
|
+
- **Service layer** validates and creates clients
|
|
353
|
+
- **Methods** follow a consistent pattern
|
|
354
|
+
- **No manual filtering** - let JSON.stringify handle it
|
|
355
|
+
|
|
356
|
+
This pattern ensures consistency, maintainability, and clarity across all namespaces in the SDK.
|
|
357
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-cloud",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Work in progress. A WhatsApp client tailored for LLMs—built to actually work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"tsup": "^8.5.1",
|
|
15
15
|
"typescript": "^5.9.3"
|
|
16
16
|
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"zod": "^4.2.1"
|
|
19
|
+
},
|
|
17
20
|
"scripts": {
|
|
18
21
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
19
22
|
"lint": "tsc"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ClientConfig } from "../types/client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP client for making requests to the WhatsApp Cloud API
|
|
5
|
+
*/
|
|
6
|
+
export class HttpClient {
|
|
7
|
+
private readonly baseURL: string;
|
|
8
|
+
public readonly accessToken: string;
|
|
9
|
+
public readonly phoneNumberId?: string;
|
|
10
|
+
public readonly businessAccountId?: string;
|
|
11
|
+
public readonly businessId?: string;
|
|
12
|
+
public readonly apiVersion: string;
|
|
13
|
+
|
|
14
|
+
constructor(config: ClientConfig) {
|
|
15
|
+
this.accessToken = config.accessToken;
|
|
16
|
+
if (config.phoneNumberId !== undefined) {
|
|
17
|
+
this.phoneNumberId = config.phoneNumberId;
|
|
18
|
+
}
|
|
19
|
+
if (config.businessAccountId !== undefined) {
|
|
20
|
+
this.businessAccountId = config.businessAccountId;
|
|
21
|
+
}
|
|
22
|
+
if (config.businessId !== undefined) {
|
|
23
|
+
this.businessId = config.businessId;
|
|
24
|
+
}
|
|
25
|
+
this.apiVersion = config.apiVersion ?? "v18.0";
|
|
26
|
+
this.baseURL = config.baseURL ?? "https://graph.facebook.com";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Make a POST request
|
|
31
|
+
*/
|
|
32
|
+
async post<T>(path: string, body: unknown): Promise<T> {
|
|
33
|
+
const url = `${this.baseURL}/${this.apiVersion}${path}`;
|
|
34
|
+
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const error = await response.json().catch(() => ({
|
|
46
|
+
error: {
|
|
47
|
+
message: response.statusText,
|
|
48
|
+
code: response.status,
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
throw new Error(
|
|
52
|
+
`API Error: ${error.error?.message || response.statusText} (${
|
|
53
|
+
error.error?.code || response.status
|
|
54
|
+
})`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response.json() as Promise<T>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Make a GET request
|
|
63
|
+
*/
|
|
64
|
+
async get<T>(path: string): Promise<T> {
|
|
65
|
+
const url = `${this.baseURL}/${this.apiVersion}${path}`;
|
|
66
|
+
|
|
67
|
+
const response = await fetch(url, {
|
|
68
|
+
method: "GET",
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const error = await response.json().catch(() => ({
|
|
76
|
+
error: {
|
|
77
|
+
message: response.statusText,
|
|
78
|
+
code: response.status,
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
throw new Error(
|
|
82
|
+
`API Error: ${error.error?.message || response.statusText} (${
|
|
83
|
+
error.error?.code || response.status
|
|
84
|
+
})`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return response.json() as Promise<T>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Make a PATCH request
|
|
93
|
+
*/
|
|
94
|
+
async patch<T>(path: string, body: unknown): Promise<T> {
|
|
95
|
+
const url = `${this.baseURL}/${this.apiVersion}${path}`;
|
|
96
|
+
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
method: "PATCH",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
const error = await response.json().catch(() => ({
|
|
108
|
+
error: {
|
|
109
|
+
message: response.statusText,
|
|
110
|
+
code: response.status,
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
throw new Error(
|
|
114
|
+
`API Error: ${error.error?.message || response.statusText} (${
|
|
115
|
+
error.error?.code || response.status
|
|
116
|
+
})`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return response.json() as Promise<T>;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { clientConfigSchema } from "../schemas/client";
|
|
2
|
+
import type { ClientConfig } from "../types/client";
|
|
3
|
+
import { HttpClient } from "./HttpClient";
|
|
4
|
+
import { MessagesService } from "../services/messages/index";
|
|
5
|
+
import { AccountsService } from "../services/accounts/index";
|
|
6
|
+
import { BusinessService } from "../services/business/index";
|
|
7
|
+
import { ZodError } from "zod";
|
|
8
|
+
import { transformZodError } from "../utils/zod-error";
|
|
9
|
+
import type { DebugTokenResponse } from "../types/debug";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* WhatsApp Cloud API client
|
|
13
|
+
*/
|
|
14
|
+
export class WhatsAppClient {
|
|
15
|
+
public readonly messages: MessagesService;
|
|
16
|
+
public readonly accounts: AccountsService;
|
|
17
|
+
public readonly business: BusinessService;
|
|
18
|
+
|
|
19
|
+
private readonly httpClient: HttpClient;
|
|
20
|
+
|
|
21
|
+
constructor(config: ClientConfig) {
|
|
22
|
+
// Validate config with schema - Zod provides detailed error messages
|
|
23
|
+
let validated: ClientConfig;
|
|
24
|
+
try {
|
|
25
|
+
validated = clientConfigSchema.parse(config);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof ZodError) {
|
|
28
|
+
throw transformZodError(error);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Initialize HTTP client
|
|
34
|
+
this.httpClient = new HttpClient(validated);
|
|
35
|
+
|
|
36
|
+
// Initialize services (namespaces)
|
|
37
|
+
this.messages = new MessagesService(this.httpClient);
|
|
38
|
+
this.accounts = new AccountsService(this.httpClient);
|
|
39
|
+
this.business = new BusinessService(this.httpClient);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Debug the current access token
|
|
44
|
+
*
|
|
45
|
+
* This method calls the Graph API debug_token endpoint to inspect the access token
|
|
46
|
+
* used by this client. Useful for understanding token permissions, expiration, and validity.
|
|
47
|
+
*
|
|
48
|
+
* @returns Debug information about the access token
|
|
49
|
+
*/
|
|
50
|
+
async debugToken(): Promise<DebugTokenResponse> {
|
|
51
|
+
return this.httpClient.get<DebugTokenResponse>(
|
|
52
|
+
`/debug_token?input_token=${this.httpClient.accessToken}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for WhatsApp API errors
|
|
3
|
+
*/
|
|
4
|
+
export class WhatsAppError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
9
|
+
const captureStackTrace = (Error as any).captureStackTrace;
|
|
10
|
+
if (typeof captureStackTrace === "function") {
|
|
11
|
+
captureStackTrace(this, this.constructor);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when validation fails (configuration, requests, etc.)
|
|
18
|
+
* Can be used for any Zod validation error
|
|
19
|
+
*/
|
|
20
|
+
export class WhatsAppValidationError extends WhatsAppError {
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
public readonly field?: string,
|
|
24
|
+
public readonly issues?: Array<{
|
|
25
|
+
path: readonly (string | number)[];
|
|
26
|
+
message: string;
|
|
27
|
+
}>
|
|
28
|
+
) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "WhatsAppValidationError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when an API request fails
|
|
36
|
+
*/
|
|
37
|
+
export class WhatsAppAPIError extends WhatsAppError {
|
|
38
|
+
constructor(
|
|
39
|
+
public readonly code: number,
|
|
40
|
+
public readonly type: string,
|
|
41
|
+
message: string,
|
|
42
|
+
public readonly statusCode?: number,
|
|
43
|
+
public readonly details?: unknown
|
|
44
|
+
) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "WhatsAppAPIError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error thrown when rate limit is exceeded
|
|
52
|
+
*/
|
|
53
|
+
export class WhatsAppRateLimitError extends WhatsAppAPIError {
|
|
54
|
+
constructor(message: string, public readonly retryAfter?: number) {
|
|
55
|
+
super(131056, "rate_limit", message, 429, { retryAfter });
|
|
56
|
+
this.name = "WhatsAppRateLimitError";
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
// WhatsApp Cloud API SDK
|
|
2
|
+
export { WhatsAppClient } from "./client/index";
|
|
3
|
+
|
|
4
|
+
// Export schemas (AI-ready)
|
|
5
|
+
export * from "./schemas/index";
|
|
6
|
+
|
|
7
|
+
// Export types (primary export point)
|
|
8
|
+
export type * from "./types/index";
|
|
9
|
+
|
|
10
|
+
// Export errors for error handling
|
|
11
|
+
export {
|
|
12
|
+
WhatsAppError,
|
|
13
|
+
WhatsAppValidationError,
|
|
14
|
+
WhatsAppAPIError,
|
|
15
|
+
WhatsAppRateLimitError,
|
|
16
|
+
} from "./errors";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./phone-number";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for phone number response
|
|
5
|
+
* Matches WhatsApp API structure for phone number objects
|
|
6
|
+
*/
|
|
7
|
+
export const phoneNumberResponseSchema = z.object({
|
|
8
|
+
verified_name: z.string(),
|
|
9
|
+
display_phone_number: z.string(),
|
|
10
|
+
id: z.string(),
|
|
11
|
+
quality_rating: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Schema for phone number list response
|
|
16
|
+
* Matches WhatsApp API structure for GET /phone_numbers endpoint
|
|
17
|
+
*/
|
|
18
|
+
export const phoneNumberListResponseSchema = z.object({
|
|
19
|
+
data: z.array(phoneNumberResponseSchema),
|
|
20
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Schema for WhatsApp Business Account (WABA) response
|
|
5
|
+
* Matches WhatsApp API structure for WABA objects
|
|
6
|
+
*/
|
|
7
|
+
export const businessAccountResponseSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
name: z.string().optional(),
|
|
10
|
+
account_review_status: z.string().optional(),
|
|
11
|
+
currency: z.string().optional(),
|
|
12
|
+
country: z.string().optional(),
|
|
13
|
+
timezone_id: z.string().optional(),
|
|
14
|
+
business_verification_status: z.string().optional(),
|
|
15
|
+
is_enabled_for_insights: z.boolean().optional(),
|
|
16
|
+
message_template_namespace: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Schema for WhatsApp Business Accounts list response
|
|
21
|
+
* Matches WhatsApp API structure for GET /whatsapp_business_accounts endpoint
|
|
22
|
+
*
|
|
23
|
+
* Note: The API returns data as an object with numeric string keys (e.g., "0", "1")
|
|
24
|
+
* or as an array, plus optional paging information
|
|
25
|
+
*/
|
|
26
|
+
export const businessAccountsListResponseSchema = z.object({
|
|
27
|
+
data: z.record(z.string(), businessAccountResponseSchema).or(
|
|
28
|
+
z.array(businessAccountResponseSchema)
|
|
29
|
+
),
|
|
30
|
+
paging: z
|
|
31
|
+
.object({
|
|
32
|
+
cursors: z
|
|
33
|
+
.object({
|
|
34
|
+
before: z.string().optional(),
|
|
35
|
+
after: z.string().optional(),
|
|
36
|
+
})
|
|
37
|
+
.optional(),
|
|
38
|
+
next: z.string().url().optional(),
|
|
39
|
+
previous: z.string().url().optional(),
|
|
40
|
+
})
|
|
41
|
+
.optional(),
|
|
42
|
+
});
|
|
43
|
+
|