whatsapp-cloud 0.1.3 → 0.1.5

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # whatzapp
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 32573ff: add media download namespace
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 0741aba: add sendMessage method
14
+
3
15
  ## 0.1.3
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -81,7 +81,7 @@ export async function POST(request: NextRequest) {
81
81
  console.log(`Image from ${context.contact?.name || message.from}`);
82
82
 
83
83
  // Download the image
84
- const imageData = await client.webhooks.downloadMedia(message.image.id);
84
+ const imageData = await client.media.download(message.image.id);
85
85
 
86
86
  // Process the image (save to storage, analyze, etc.)
87
87
  // const buffer = Buffer.from(imageData);
@@ -107,4 +107,5 @@ export async function POST(request: NextRequest) {
107
107
  - `client.accounts` - Manage WhatsApp Business Accounts
108
108
  - `client.business` - Manage Business Portfolios
109
109
  - `client.templates` - Create, retrieve and send WhatsApp templates
110
+ - `client.media` - Upload, download, and manage media files
110
111
  - ... more to come very soon. 🕒
package/dist/index.cjs CHANGED
@@ -503,6 +503,24 @@ var MessagesService = class {
503
503
  const client = this.getClient(phoneNumberId);
504
504
  return sendReaction(client, input);
505
505
  }
506
+ /**
507
+ * Send any message type using the discriminated union
508
+ *
509
+ * @param message - Any outgoing message (text, image, location, reaction)
510
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
511
+ */
512
+ async sendMessage(message, phoneNumberId) {
513
+ switch (message.type) {
514
+ case "text":
515
+ return this.sendText(message, phoneNumberId);
516
+ case "image":
517
+ return this.sendImage(message, phoneNumberId);
518
+ case "location":
519
+ return this.sendLocation(message, phoneNumberId);
520
+ case "reaction":
521
+ return this.sendReaction(message, phoneNumberId);
522
+ }
523
+ }
506
524
  };
507
525
 
508
526
  // src/services/accounts/AccountsClient.ts
@@ -1401,33 +1419,6 @@ var WebhooksService = class {
1401
1419
  extractStatuses(payload) {
1402
1420
  return extractStatuses(payload);
1403
1421
  }
1404
- /**
1405
- * Download media file by media ID
1406
- *
1407
- * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1408
- * Uses the access token from the client configuration automatically.
1409
- *
1410
- * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1411
- * @returns Promise resolving to ArrayBuffer containing the media file
1412
- * @throws Error if download fails or media ID is invalid
1413
- *
1414
- * @example
1415
- * ```typescript
1416
- * client.webhooks.handle(req.body, {
1417
- * image: async (message, context) => {
1418
- * const mediaData = await client.webhooks.downloadMedia(message.image.id);
1419
- * // Upload to S3, save to disk, etc.
1420
- * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1421
- * },
1422
- * });
1423
- * ```
1424
- */
1425
- async downloadMedia(mediaId) {
1426
- if (!mediaId || mediaId.trim().length === 0) {
1427
- throw new Error("Media ID is required");
1428
- }
1429
- return this.httpClient.getBinary(`/${mediaId}`);
1430
- }
1431
1422
  /**
1432
1423
  * Validate webhook payload structure
1433
1424
  *
@@ -1543,6 +1534,57 @@ var WebhooksService = class {
1543
1534
  }
1544
1535
  };
1545
1536
 
1537
+ // src/services/media/MediaService.ts
1538
+ var MediaService = class {
1539
+ constructor(httpClient) {
1540
+ this.httpClient = httpClient;
1541
+ }
1542
+ /**
1543
+ * Download media file by media ID
1544
+ *
1545
+ * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1546
+ * Uses the access token from the client configuration automatically.
1547
+ *
1548
+ * According to WhatsApp API docs, you cannot download directly from the media ID endpoint.
1549
+ * The flow is:
1550
+ * 1. GET /MEDIA_ID → returns JSON metadata with a URL
1551
+ * 2. GET /MEDIA_URL → returns the actual binary data
1552
+ *
1553
+ * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1554
+ * @returns Promise resolving to ArrayBuffer containing the media file
1555
+ * @throws Error if download fails or media ID is invalid
1556
+ *
1557
+ * @example
1558
+ * ```typescript
1559
+ * const mediaData = await client.media.download(message.image.id);
1560
+ * // Upload to S3, save to disk, etc.
1561
+ * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1562
+ * ```
1563
+ */
1564
+ async download(mediaId) {
1565
+ if (!mediaId || mediaId.trim().length === 0) {
1566
+ throw new Error("Media ID is required");
1567
+ }
1568
+ const metadata = await this.httpClient.get(`/${mediaId}`);
1569
+ const response = await fetch(metadata.url, {
1570
+ method: "GET",
1571
+ headers: {
1572
+ Authorization: `Bearer ${this.httpClient.accessToken}`
1573
+ }
1574
+ });
1575
+ if (!response.ok) {
1576
+ let errorMessage = `API Error: ${response.statusText}`;
1577
+ try {
1578
+ const error = await response.json();
1579
+ errorMessage = `API Error: ${error.error?.message || response.statusText} (${error.error?.code || response.status})`;
1580
+ } catch {
1581
+ }
1582
+ throw new Error(errorMessage);
1583
+ }
1584
+ return response.arrayBuffer();
1585
+ }
1586
+ };
1587
+
1546
1588
  // src/client/WhatsAppClient.ts
1547
1589
  var import_zod9 = require("zod");
1548
1590
  var WhatsAppClient = class {
@@ -1551,6 +1593,7 @@ var WhatsAppClient = class {
1551
1593
  business;
1552
1594
  templates;
1553
1595
  webhooks;
1596
+ media;
1554
1597
  httpClient;
1555
1598
  constructor(config) {
1556
1599
  let validated;
@@ -1568,6 +1611,7 @@ var WhatsAppClient = class {
1568
1611
  this.business = new BusinessService(this.httpClient);
1569
1612
  this.templates = new TemplatesService(this.httpClient);
1570
1613
  this.webhooks = new WebhooksService(this.httpClient);
1614
+ this.media = new MediaService(this.httpClient);
1571
1615
  }
1572
1616
  /**
1573
1617
  * Debug the current access token
package/dist/index.d.cts CHANGED
@@ -279,6 +279,13 @@ declare class MessagesService {
279
279
  * @param phoneNumberId - Optional phone number ID (overrides client config)
280
280
  */
281
281
  sendReaction(input: SendReactionInput, phoneNumberId?: string): Promise<MessageResponse>;
282
+ /**
283
+ * Send any message type using the discriminated union
284
+ *
285
+ * @param message - Any outgoing message (text, image, location, reaction)
286
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
287
+ */
288
+ sendMessage(message: OutgoingMessage, phoneNumberId?: string): Promise<MessageResponse>;
282
289
  }
283
290
 
284
291
  /**
@@ -1377,28 +1384,6 @@ declare class WebhooksService {
1377
1384
  * @returns Flat array of status updates
1378
1385
  */
1379
1386
  extractStatuses(payload: WebhookPayload): Status[];
1380
- /**
1381
- * Download media file by media ID
1382
- *
1383
- * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1384
- * Uses the access token from the client configuration automatically.
1385
- *
1386
- * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1387
- * @returns Promise resolving to ArrayBuffer containing the media file
1388
- * @throws Error if download fails or media ID is invalid
1389
- *
1390
- * @example
1391
- * ```typescript
1392
- * client.webhooks.handle(req.body, {
1393
- * image: async (message, context) => {
1394
- * const mediaData = await client.webhooks.downloadMedia(message.image.id);
1395
- * // Upload to S3, save to disk, etc.
1396
- * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1397
- * },
1398
- * });
1399
- * ```
1400
- */
1401
- downloadMedia(mediaId: string): Promise<ArrayBuffer>;
1402
1387
  /**
1403
1388
  * Validate webhook payload structure
1404
1389
  *
@@ -1429,6 +1414,39 @@ declare class WebhooksService {
1429
1414
  handle<THandlers extends MessageHandlers<any>>(payload: unknown, handlers: THandlers, options?: HandleOptions): void;
1430
1415
  }
1431
1416
 
1417
+ /**
1418
+ * Media service for downloading WhatsApp media files
1419
+ *
1420
+ * This service handles downloading media files from WhatsApp servers.
1421
+ */
1422
+ declare class MediaService {
1423
+ private readonly httpClient;
1424
+ constructor(httpClient: HttpClient);
1425
+ /**
1426
+ * Download media file by media ID
1427
+ *
1428
+ * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1429
+ * Uses the access token from the client configuration automatically.
1430
+ *
1431
+ * According to WhatsApp API docs, you cannot download directly from the media ID endpoint.
1432
+ * The flow is:
1433
+ * 1. GET /MEDIA_ID → returns JSON metadata with a URL
1434
+ * 2. GET /MEDIA_URL → returns the actual binary data
1435
+ *
1436
+ * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1437
+ * @returns Promise resolving to ArrayBuffer containing the media file
1438
+ * @throws Error if download fails or media ID is invalid
1439
+ *
1440
+ * @example
1441
+ * ```typescript
1442
+ * const mediaData = await client.media.download(message.image.id);
1443
+ * // Upload to S3, save to disk, etc.
1444
+ * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1445
+ * ```
1446
+ */
1447
+ download(mediaId: string): Promise<ArrayBuffer>;
1448
+ }
1449
+
1432
1450
  /**
1433
1451
  * Schema for debug token response
1434
1452
  * Matches Graph API debug_token endpoint response structure
@@ -1465,6 +1483,7 @@ declare class WhatsAppClient {
1465
1483
  readonly business: BusinessService;
1466
1484
  readonly templates: TemplatesService;
1467
1485
  readonly webhooks: WebhooksService;
1486
+ readonly media: MediaService;
1468
1487
  private readonly httpClient;
1469
1488
  constructor(config: ClientConfig);
1470
1489
  /**
@@ -1797,6 +1816,18 @@ type TemplateComponent = z.infer<typeof templateComponentSchema>;
1797
1816
  */
1798
1817
  type TemplateLanguage = z.infer<typeof templateLanguageSchema>;
1799
1818
 
1819
+ /**
1820
+ * Media metadata response (from GET /MEDIA_ID)
1821
+ */
1822
+ type MediaMetadata = {
1823
+ messaging_product: "whatsapp";
1824
+ url: string;
1825
+ mime_type: string;
1826
+ sha256: string;
1827
+ file_size: string;
1828
+ id: string;
1829
+ };
1830
+
1800
1831
  /**
1801
1832
  * Base error class for WhatsApp API errors
1802
1833
  */
@@ -1836,4 +1867,4 @@ declare class WhatsAppRateLimitError extends WhatsAppAPIError {
1836
1867
  constructor(message: string, retryAfter?: number | undefined);
1837
1868
  }
1838
1869
 
1839
- export { type BusinessAccountsListResponse, type ClientConfig, type DebugTokenResponse, type HandleOptions, type IncomingAudioMessage, type IncomingImageMessage, type IncomingMessage, type IncomingTextMessage, type MessageContext, type MessageHandlers, type MessageResponse, type OutgoingImageMessage, type OutgoingLocationMessage, type OutgoingMessage, type OutgoingReactionMessage, type OutgoingTextMessage, type PhoneNumberListResponse, type SendImageInput, type SendLocationInput, type SendReactionInput, type SendTextInput, type Status, type Template, type TemplateBodyComponent, type TemplateButton, type TemplateButtonsComponent, type TemplateComponent, type TemplateCopyCodeButton, type TemplateCreate, type TemplateCreateResponse, type TemplateDelete, type TemplateDeleteResponse, type TemplateFlowButton, type TemplateFooterComponent, type TemplateHeaderComponent, type TemplateLanguage, type TemplateList, type TemplateListResponse, type TemplatePhoneNumberButton, type TemplateQuickReplyButton, type TemplateUpdate, type TemplateUpdateResponse, type TemplateUrlButton, type WebhookContext, type WebhookPayload, WhatsAppAPIError, WhatsAppClient, WhatsAppError, WhatsAppRateLimitError, WhatsAppValidationError, businessAccountResponseSchema, businessAccountsListResponseSchema, clientConfigSchema, debugTokenResponseSchema, incomingAudioMessageSchema, incomingImageMessageSchema, incomingMessageSchema, incomingTextMessageSchema, messageResponseSchema, outgoingImageMessageSchema, outgoingLocationMessageSchema, outgoingMessageSchema, outgoingReactionMessageSchema, outgoingTextMessageSchema, phoneNumberListResponseSchema, phoneNumberResponseSchema, sendImageInputSchema, sendLocationInputSchema, sendReactionInputSchema, sendTextInputSchema, statusSchema, templateBodyComponentSchema, templateButtonSchema, templateButtonsComponentSchema, templateComponentSchema, templateCopyCodeButtonSchema, templateCreateResponseSchema, templateCreateSchema, templateDeleteResponseSchema, templateDeleteSchema, templateFlowButtonSchema, templateFooterComponentSchema, templateHeaderComponentSchema, templateLanguageSchema, templateListResponseSchema, templateListSchema, templatePhoneNumberButtonSchema, templateQuickReplyButtonSchema, templateSchema, templateUpdateResponseSchema, templateUpdateSchema, templateUrlButtonSchema, webhookPayloadSchema };
1870
+ export { type BusinessAccountsListResponse, type ClientConfig, type DebugTokenResponse, type HandleOptions, type IncomingAudioMessage, type IncomingImageMessage, type IncomingMessage, type IncomingTextMessage, type MediaMetadata, type MessageContext, type MessageHandlers, type MessageResponse, type OutgoingImageMessage, type OutgoingLocationMessage, type OutgoingMessage, type OutgoingReactionMessage, type OutgoingTextMessage, type PhoneNumberListResponse, type SendImageInput, type SendLocationInput, type SendReactionInput, type SendTextInput, type Status, type Template, type TemplateBodyComponent, type TemplateButton, type TemplateButtonsComponent, type TemplateComponent, type TemplateCopyCodeButton, type TemplateCreate, type TemplateCreateResponse, type TemplateDelete, type TemplateDeleteResponse, type TemplateFlowButton, type TemplateFooterComponent, type TemplateHeaderComponent, type TemplateLanguage, type TemplateList, type TemplateListResponse, type TemplatePhoneNumberButton, type TemplateQuickReplyButton, type TemplateUpdate, type TemplateUpdateResponse, type TemplateUrlButton, type WebhookContext, type WebhookPayload, WhatsAppAPIError, WhatsAppClient, WhatsAppError, WhatsAppRateLimitError, WhatsAppValidationError, businessAccountResponseSchema, businessAccountsListResponseSchema, clientConfigSchema, debugTokenResponseSchema, incomingAudioMessageSchema, incomingImageMessageSchema, incomingMessageSchema, incomingTextMessageSchema, messageResponseSchema, outgoingImageMessageSchema, outgoingLocationMessageSchema, outgoingMessageSchema, outgoingReactionMessageSchema, outgoingTextMessageSchema, phoneNumberListResponseSchema, phoneNumberResponseSchema, sendImageInputSchema, sendLocationInputSchema, sendReactionInputSchema, sendTextInputSchema, statusSchema, templateBodyComponentSchema, templateButtonSchema, templateButtonsComponentSchema, templateComponentSchema, templateCopyCodeButtonSchema, templateCreateResponseSchema, templateCreateSchema, templateDeleteResponseSchema, templateDeleteSchema, templateFlowButtonSchema, templateFooterComponentSchema, templateHeaderComponentSchema, templateLanguageSchema, templateListResponseSchema, templateListSchema, templatePhoneNumberButtonSchema, templateQuickReplyButtonSchema, templateSchema, templateUpdateResponseSchema, templateUpdateSchema, templateUrlButtonSchema, webhookPayloadSchema };
package/dist/index.d.ts CHANGED
@@ -279,6 +279,13 @@ declare class MessagesService {
279
279
  * @param phoneNumberId - Optional phone number ID (overrides client config)
280
280
  */
281
281
  sendReaction(input: SendReactionInput, phoneNumberId?: string): Promise<MessageResponse>;
282
+ /**
283
+ * Send any message type using the discriminated union
284
+ *
285
+ * @param message - Any outgoing message (text, image, location, reaction)
286
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
287
+ */
288
+ sendMessage(message: OutgoingMessage, phoneNumberId?: string): Promise<MessageResponse>;
282
289
  }
283
290
 
284
291
  /**
@@ -1377,28 +1384,6 @@ declare class WebhooksService {
1377
1384
  * @returns Flat array of status updates
1378
1385
  */
1379
1386
  extractStatuses(payload: WebhookPayload): Status[];
1380
- /**
1381
- * Download media file by media ID
1382
- *
1383
- * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1384
- * Uses the access token from the client configuration automatically.
1385
- *
1386
- * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1387
- * @returns Promise resolving to ArrayBuffer containing the media file
1388
- * @throws Error if download fails or media ID is invalid
1389
- *
1390
- * @example
1391
- * ```typescript
1392
- * client.webhooks.handle(req.body, {
1393
- * image: async (message, context) => {
1394
- * const mediaData = await client.webhooks.downloadMedia(message.image.id);
1395
- * // Upload to S3, save to disk, etc.
1396
- * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1397
- * },
1398
- * });
1399
- * ```
1400
- */
1401
- downloadMedia(mediaId: string): Promise<ArrayBuffer>;
1402
1387
  /**
1403
1388
  * Validate webhook payload structure
1404
1389
  *
@@ -1429,6 +1414,39 @@ declare class WebhooksService {
1429
1414
  handle<THandlers extends MessageHandlers<any>>(payload: unknown, handlers: THandlers, options?: HandleOptions): void;
1430
1415
  }
1431
1416
 
1417
+ /**
1418
+ * Media service for downloading WhatsApp media files
1419
+ *
1420
+ * This service handles downloading media files from WhatsApp servers.
1421
+ */
1422
+ declare class MediaService {
1423
+ private readonly httpClient;
1424
+ constructor(httpClient: HttpClient);
1425
+ /**
1426
+ * Download media file by media ID
1427
+ *
1428
+ * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1429
+ * Uses the access token from the client configuration automatically.
1430
+ *
1431
+ * According to WhatsApp API docs, you cannot download directly from the media ID endpoint.
1432
+ * The flow is:
1433
+ * 1. GET /MEDIA_ID → returns JSON metadata with a URL
1434
+ * 2. GET /MEDIA_URL → returns the actual binary data
1435
+ *
1436
+ * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1437
+ * @returns Promise resolving to ArrayBuffer containing the media file
1438
+ * @throws Error if download fails or media ID is invalid
1439
+ *
1440
+ * @example
1441
+ * ```typescript
1442
+ * const mediaData = await client.media.download(message.image.id);
1443
+ * // Upload to S3, save to disk, etc.
1444
+ * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1445
+ * ```
1446
+ */
1447
+ download(mediaId: string): Promise<ArrayBuffer>;
1448
+ }
1449
+
1432
1450
  /**
1433
1451
  * Schema for debug token response
1434
1452
  * Matches Graph API debug_token endpoint response structure
@@ -1465,6 +1483,7 @@ declare class WhatsAppClient {
1465
1483
  readonly business: BusinessService;
1466
1484
  readonly templates: TemplatesService;
1467
1485
  readonly webhooks: WebhooksService;
1486
+ readonly media: MediaService;
1468
1487
  private readonly httpClient;
1469
1488
  constructor(config: ClientConfig);
1470
1489
  /**
@@ -1797,6 +1816,18 @@ type TemplateComponent = z.infer<typeof templateComponentSchema>;
1797
1816
  */
1798
1817
  type TemplateLanguage = z.infer<typeof templateLanguageSchema>;
1799
1818
 
1819
+ /**
1820
+ * Media metadata response (from GET /MEDIA_ID)
1821
+ */
1822
+ type MediaMetadata = {
1823
+ messaging_product: "whatsapp";
1824
+ url: string;
1825
+ mime_type: string;
1826
+ sha256: string;
1827
+ file_size: string;
1828
+ id: string;
1829
+ };
1830
+
1800
1831
  /**
1801
1832
  * Base error class for WhatsApp API errors
1802
1833
  */
@@ -1836,4 +1867,4 @@ declare class WhatsAppRateLimitError extends WhatsAppAPIError {
1836
1867
  constructor(message: string, retryAfter?: number | undefined);
1837
1868
  }
1838
1869
 
1839
- export { type BusinessAccountsListResponse, type ClientConfig, type DebugTokenResponse, type HandleOptions, type IncomingAudioMessage, type IncomingImageMessage, type IncomingMessage, type IncomingTextMessage, type MessageContext, type MessageHandlers, type MessageResponse, type OutgoingImageMessage, type OutgoingLocationMessage, type OutgoingMessage, type OutgoingReactionMessage, type OutgoingTextMessage, type PhoneNumberListResponse, type SendImageInput, type SendLocationInput, type SendReactionInput, type SendTextInput, type Status, type Template, type TemplateBodyComponent, type TemplateButton, type TemplateButtonsComponent, type TemplateComponent, type TemplateCopyCodeButton, type TemplateCreate, type TemplateCreateResponse, type TemplateDelete, type TemplateDeleteResponse, type TemplateFlowButton, type TemplateFooterComponent, type TemplateHeaderComponent, type TemplateLanguage, type TemplateList, type TemplateListResponse, type TemplatePhoneNumberButton, type TemplateQuickReplyButton, type TemplateUpdate, type TemplateUpdateResponse, type TemplateUrlButton, type WebhookContext, type WebhookPayload, WhatsAppAPIError, WhatsAppClient, WhatsAppError, WhatsAppRateLimitError, WhatsAppValidationError, businessAccountResponseSchema, businessAccountsListResponseSchema, clientConfigSchema, debugTokenResponseSchema, incomingAudioMessageSchema, incomingImageMessageSchema, incomingMessageSchema, incomingTextMessageSchema, messageResponseSchema, outgoingImageMessageSchema, outgoingLocationMessageSchema, outgoingMessageSchema, outgoingReactionMessageSchema, outgoingTextMessageSchema, phoneNumberListResponseSchema, phoneNumberResponseSchema, sendImageInputSchema, sendLocationInputSchema, sendReactionInputSchema, sendTextInputSchema, statusSchema, templateBodyComponentSchema, templateButtonSchema, templateButtonsComponentSchema, templateComponentSchema, templateCopyCodeButtonSchema, templateCreateResponseSchema, templateCreateSchema, templateDeleteResponseSchema, templateDeleteSchema, templateFlowButtonSchema, templateFooterComponentSchema, templateHeaderComponentSchema, templateLanguageSchema, templateListResponseSchema, templateListSchema, templatePhoneNumberButtonSchema, templateQuickReplyButtonSchema, templateSchema, templateUpdateResponseSchema, templateUpdateSchema, templateUrlButtonSchema, webhookPayloadSchema };
1870
+ export { type BusinessAccountsListResponse, type ClientConfig, type DebugTokenResponse, type HandleOptions, type IncomingAudioMessage, type IncomingImageMessage, type IncomingMessage, type IncomingTextMessage, type MediaMetadata, type MessageContext, type MessageHandlers, type MessageResponse, type OutgoingImageMessage, type OutgoingLocationMessage, type OutgoingMessage, type OutgoingReactionMessage, type OutgoingTextMessage, type PhoneNumberListResponse, type SendImageInput, type SendLocationInput, type SendReactionInput, type SendTextInput, type Status, type Template, type TemplateBodyComponent, type TemplateButton, type TemplateButtonsComponent, type TemplateComponent, type TemplateCopyCodeButton, type TemplateCreate, type TemplateCreateResponse, type TemplateDelete, type TemplateDeleteResponse, type TemplateFlowButton, type TemplateFooterComponent, type TemplateHeaderComponent, type TemplateLanguage, type TemplateList, type TemplateListResponse, type TemplatePhoneNumberButton, type TemplateQuickReplyButton, type TemplateUpdate, type TemplateUpdateResponse, type TemplateUrlButton, type WebhookContext, type WebhookPayload, WhatsAppAPIError, WhatsAppClient, WhatsAppError, WhatsAppRateLimitError, WhatsAppValidationError, businessAccountResponseSchema, businessAccountsListResponseSchema, clientConfigSchema, debugTokenResponseSchema, incomingAudioMessageSchema, incomingImageMessageSchema, incomingMessageSchema, incomingTextMessageSchema, messageResponseSchema, outgoingImageMessageSchema, outgoingLocationMessageSchema, outgoingMessageSchema, outgoingReactionMessageSchema, outgoingTextMessageSchema, phoneNumberListResponseSchema, phoneNumberResponseSchema, sendImageInputSchema, sendLocationInputSchema, sendReactionInputSchema, sendTextInputSchema, statusSchema, templateBodyComponentSchema, templateButtonSchema, templateButtonsComponentSchema, templateComponentSchema, templateCopyCodeButtonSchema, templateCreateResponseSchema, templateCreateSchema, templateDeleteResponseSchema, templateDeleteSchema, templateFlowButtonSchema, templateFooterComponentSchema, templateHeaderComponentSchema, templateLanguageSchema, templateListResponseSchema, templateListSchema, templatePhoneNumberButtonSchema, templateQuickReplyButtonSchema, templateSchema, templateUpdateResponseSchema, templateUpdateSchema, templateUrlButtonSchema, webhookPayloadSchema };
package/dist/index.js CHANGED
@@ -430,6 +430,24 @@ var MessagesService = class {
430
430
  const client = this.getClient(phoneNumberId);
431
431
  return sendReaction(client, input);
432
432
  }
433
+ /**
434
+ * Send any message type using the discriminated union
435
+ *
436
+ * @param message - Any outgoing message (text, image, location, reaction)
437
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
438
+ */
439
+ async sendMessage(message, phoneNumberId) {
440
+ switch (message.type) {
441
+ case "text":
442
+ return this.sendText(message, phoneNumberId);
443
+ case "image":
444
+ return this.sendImage(message, phoneNumberId);
445
+ case "location":
446
+ return this.sendLocation(message, phoneNumberId);
447
+ case "reaction":
448
+ return this.sendReaction(message, phoneNumberId);
449
+ }
450
+ }
433
451
  };
434
452
 
435
453
  // src/services/accounts/AccountsClient.ts
@@ -1328,33 +1346,6 @@ var WebhooksService = class {
1328
1346
  extractStatuses(payload) {
1329
1347
  return extractStatuses(payload);
1330
1348
  }
1331
- /**
1332
- * Download media file by media ID
1333
- *
1334
- * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1335
- * Uses the access token from the client configuration automatically.
1336
- *
1337
- * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1338
- * @returns Promise resolving to ArrayBuffer containing the media file
1339
- * @throws Error if download fails or media ID is invalid
1340
- *
1341
- * @example
1342
- * ```typescript
1343
- * client.webhooks.handle(req.body, {
1344
- * image: async (message, context) => {
1345
- * const mediaData = await client.webhooks.downloadMedia(message.image.id);
1346
- * // Upload to S3, save to disk, etc.
1347
- * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1348
- * },
1349
- * });
1350
- * ```
1351
- */
1352
- async downloadMedia(mediaId) {
1353
- if (!mediaId || mediaId.trim().length === 0) {
1354
- throw new Error("Media ID is required");
1355
- }
1356
- return this.httpClient.getBinary(`/${mediaId}`);
1357
- }
1358
1349
  /**
1359
1350
  * Validate webhook payload structure
1360
1351
  *
@@ -1470,6 +1461,57 @@ var WebhooksService = class {
1470
1461
  }
1471
1462
  };
1472
1463
 
1464
+ // src/services/media/MediaService.ts
1465
+ var MediaService = class {
1466
+ constructor(httpClient) {
1467
+ this.httpClient = httpClient;
1468
+ }
1469
+ /**
1470
+ * Download media file by media ID
1471
+ *
1472
+ * Downloads media files (images, audio, video, documents) from WhatsApp servers.
1473
+ * Uses the access token from the client configuration automatically.
1474
+ *
1475
+ * According to WhatsApp API docs, you cannot download directly from the media ID endpoint.
1476
+ * The flow is:
1477
+ * 1. GET /MEDIA_ID → returns JSON metadata with a URL
1478
+ * 2. GET /MEDIA_URL → returns the actual binary data
1479
+ *
1480
+ * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
1481
+ * @returns Promise resolving to ArrayBuffer containing the media file
1482
+ * @throws Error if download fails or media ID is invalid
1483
+ *
1484
+ * @example
1485
+ * ```typescript
1486
+ * const mediaData = await client.media.download(message.image.id);
1487
+ * // Upload to S3, save to disk, etc.
1488
+ * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
1489
+ * ```
1490
+ */
1491
+ async download(mediaId) {
1492
+ if (!mediaId || mediaId.trim().length === 0) {
1493
+ throw new Error("Media ID is required");
1494
+ }
1495
+ const metadata = await this.httpClient.get(`/${mediaId}`);
1496
+ const response = await fetch(metadata.url, {
1497
+ method: "GET",
1498
+ headers: {
1499
+ Authorization: `Bearer ${this.httpClient.accessToken}`
1500
+ }
1501
+ });
1502
+ if (!response.ok) {
1503
+ let errorMessage = `API Error: ${response.statusText}`;
1504
+ try {
1505
+ const error = await response.json();
1506
+ errorMessage = `API Error: ${error.error?.message || response.statusText} (${error.error?.code || response.status})`;
1507
+ } catch {
1508
+ }
1509
+ throw new Error(errorMessage);
1510
+ }
1511
+ return response.arrayBuffer();
1512
+ }
1513
+ };
1514
+
1473
1515
  // src/client/WhatsAppClient.ts
1474
1516
  import { ZodError as ZodError2 } from "zod";
1475
1517
  var WhatsAppClient = class {
@@ -1478,6 +1520,7 @@ var WhatsAppClient = class {
1478
1520
  business;
1479
1521
  templates;
1480
1522
  webhooks;
1523
+ media;
1481
1524
  httpClient;
1482
1525
  constructor(config) {
1483
1526
  let validated;
@@ -1495,6 +1538,7 @@ var WhatsAppClient = class {
1495
1538
  this.business = new BusinessService(this.httpClient);
1496
1539
  this.templates = new TemplatesService(this.httpClient);
1497
1540
  this.webhooks = new WebhooksService(this.httpClient);
1541
+ this.media = new MediaService(this.httpClient);
1498
1542
  }
1499
1543
  /**
1500
1544
  * Debug the current access token
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-cloud",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -21,7 +21,7 @@
21
21
  "zod": "^4.2.1"
22
22
  },
23
23
  "scripts": {
24
- "build": "tsup src/index.ts --format cjs,esm --dts",
24
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
25
25
  "lint": "tsc",
26
26
  "example": "tsx src/examples/main.ts"
27
27
  }
@@ -6,6 +6,7 @@ import { AccountsService } from "../services/accounts/index";
6
6
  import { BusinessService } from "../services/business/index";
7
7
  import { TemplatesService } from "../services/templates/index";
8
8
  import { WebhooksService } from "../services/webhooks/index";
9
+ import { MediaService } from "../services/media/index";
9
10
  import { ZodError } from "zod";
10
11
  import { transformZodError } from "../utils/zod-error";
11
12
  import type { DebugTokenResponse } from "../types/debug";
@@ -19,6 +20,7 @@ export class WhatsAppClient {
19
20
  public readonly business: BusinessService;
20
21
  public readonly templates: TemplatesService;
21
22
  public readonly webhooks: WebhooksService;
23
+ public readonly media: MediaService;
22
24
 
23
25
  private readonly httpClient: HttpClient;
24
26
 
@@ -43,6 +45,7 @@ export class WhatsAppClient {
43
45
  this.business = new BusinessService(this.httpClient);
44
46
  this.templates = new TemplatesService(this.httpClient);
45
47
  this.webhooks = new WebhooksService(this.httpClient);
48
+ this.media = new MediaService(this.httpClient);
46
49
  }
47
50
 
48
51
  /**
@@ -0,0 +1,65 @@
1
+ import "dotenv/config";
2
+ import { WhatsAppClient } from "../client";
3
+ import { writeFileSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const client = new WhatsAppClient({
7
+ accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
8
+ });
9
+
10
+ async function testMediaDownload() {
11
+ try {
12
+ console.log("🧪 Testing Media Download API...\n");
13
+
14
+ // Media ID from the user's message
15
+ const mediaId = "768583052204368";
16
+
17
+ console.log(`📥 Downloading media with ID: ${mediaId}`);
18
+ console.log(" This should download the image from the message...\n");
19
+
20
+ // Download the media
21
+ const startTime = Date.now();
22
+ const mediaData = await client.media.download(mediaId);
23
+ const downloadTime = Date.now() - startTime;
24
+
25
+ console.log("✅ Download successful!");
26
+ console.log(` Size: ${mediaData.byteLength} bytes (${(mediaData.byteLength / 1024).toFixed(2)} KB)`);
27
+ console.log(` Time: ${downloadTime}ms\n`);
28
+
29
+ // Save to file for verification
30
+ const outputPath = join(process.cwd(), "downloaded-media.jpg");
31
+ const buffer = Buffer.from(mediaData);
32
+ writeFileSync(outputPath, buffer);
33
+
34
+ console.log(`💾 Saved to: ${outputPath}`);
35
+ console.log(" You can open this file to verify the download worked correctly.\n");
36
+
37
+ // Try to get metadata as well (if we had a getUrl method, but we can test the internal call)
38
+ console.log("📋 Testing metadata retrieval...");
39
+ try {
40
+ // We can test the internal get call by checking what we got
41
+ // Actually, let's just verify the download worked by checking file size
42
+ if (mediaData.byteLength > 0) {
43
+ console.log("✅ Media file is valid (non-empty)");
44
+ } else {
45
+ console.log("⚠️ Warning: Media file is empty");
46
+ }
47
+ } catch (error) {
48
+ console.error("❌ Error getting metadata:", error);
49
+ }
50
+
51
+ console.log("\n✨ Test completed successfully!");
52
+ } catch (error) {
53
+ console.error("\n❌ Test failed:");
54
+ if (error instanceof Error) {
55
+ console.error(` Error: ${error.message}`);
56
+ console.error(` Stack: ${error.stack}`);
57
+ } else {
58
+ console.error(" Unknown error:", error);
59
+ }
60
+ process.exit(1);
61
+ }
62
+ }
63
+
64
+ testMediaDownload();
65
+
@@ -1,2 +1,3 @@
1
1
  export * from "./messages/index";
2
2
  export * from "./accounts/index";
3
+ export * from "./media/index";
@@ -0,0 +1,71 @@
1
+ import type { HttpClient } from "../../client/HttpClient";
2
+ import type { MediaMetadata } from "../../types/media";
3
+
4
+ /**
5
+ * Media service for downloading WhatsApp media files
6
+ *
7
+ * This service handles downloading media files from WhatsApp servers.
8
+ */
9
+ export class MediaService {
10
+ constructor(private readonly httpClient: HttpClient) {}
11
+
12
+ /**
13
+ * Download media file by media ID
14
+ *
15
+ * Downloads media files (images, audio, video, documents) from WhatsApp servers.
16
+ * Uses the access token from the client configuration automatically.
17
+ *
18
+ * According to WhatsApp API docs, you cannot download directly from the media ID endpoint.
19
+ * The flow is:
20
+ * 1. GET /MEDIA_ID → returns JSON metadata with a URL
21
+ * 2. GET /MEDIA_URL → returns the actual binary data
22
+ *
23
+ * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
24
+ * @returns Promise resolving to ArrayBuffer containing the media file
25
+ * @throws Error if download fails or media ID is invalid
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const mediaData = await client.media.download(message.image.id);
30
+ * // Upload to S3, save to disk, etc.
31
+ * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
32
+ * ```
33
+ */
34
+ async download(mediaId: string): Promise<ArrayBuffer> {
35
+ if (!mediaId || mediaId.trim().length === 0) {
36
+ throw new Error("Media ID is required");
37
+ }
38
+
39
+ // Step 1: Get the media URL and metadata
40
+ // GET /MEDIA_ID returns JSON, not binary, so we use get() not getBinary()
41
+ // phone_number_id query param is optional per WhatsApp docs - we omit it
42
+ const metadata = await this.httpClient.get<MediaMetadata>(`/${mediaId}`);
43
+
44
+ // Step 2: Download the actual binary data from the URL
45
+ // The URL is a full URL (not a path), so we can't use httpClient.getBinary()
46
+ // which expects a path. We need to fetch the full URL directly.
47
+ const response = await fetch(metadata.url, {
48
+ method: "GET",
49
+ headers: {
50
+ Authorization: `Bearer ${this.httpClient.accessToken}`,
51
+ },
52
+ });
53
+
54
+ if (!response.ok) {
55
+ let errorMessage = `API Error: ${response.statusText}`;
56
+ try {
57
+ const error = (await response.json()) as {
58
+ error?: { message?: string; code?: number };
59
+ };
60
+ errorMessage = `API Error: ${
61
+ error.error?.message || response.statusText
62
+ } (${error.error?.code || response.status})`;
63
+ } catch {
64
+ // If JSON parsing fails, use default message
65
+ }
66
+ throw new Error(errorMessage);
67
+ }
68
+
69
+ return response.arrayBuffer();
70
+ }
71
+ }
@@ -0,0 +1 @@
1
+ export { MediaService } from "./MediaService";
@@ -10,6 +10,7 @@ import type {
10
10
  SendImageInput,
11
11
  SendLocationInput,
12
12
  SendReactionInput,
13
+ OutgoingMessage,
13
14
  } from "../../types/messages/outgoing";
14
15
  import type { MessageResponse } from "../../types/messages/response";
15
16
 
@@ -94,4 +95,26 @@ export class MessagesService {
94
95
  const client = this.getClient(phoneNumberId);
95
96
  return sendReaction(client, input);
96
97
  }
98
+
99
+ /**
100
+ * Send any message type using the discriminated union
101
+ *
102
+ * @param message - Any outgoing message (text, image, location, reaction)
103
+ * @param phoneNumberId - Optional phone number ID (overrides client config)
104
+ */
105
+ async sendMessage(
106
+ message: OutgoingMessage,
107
+ phoneNumberId?: string
108
+ ): Promise<MessageResponse> {
109
+ switch (message.type) {
110
+ case "text":
111
+ return this.sendText(message, phoneNumberId);
112
+ case "image":
113
+ return this.sendImage(message, phoneNumberId);
114
+ case "location":
115
+ return this.sendLocation(message, phoneNumberId);
116
+ case "reaction":
117
+ return this.sendReaction(message, phoneNumberId);
118
+ }
119
+ }
97
120
  }
@@ -168,37 +168,6 @@ export class WebhooksService {
168
168
  return extractStatuses(payload);
169
169
  }
170
170
 
171
- /**
172
- * Download media file by media ID
173
- *
174
- * Downloads media files (images, audio, video, documents) from WhatsApp servers.
175
- * Uses the access token from the client configuration automatically.
176
- *
177
- * @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
178
- * @returns Promise resolving to ArrayBuffer containing the media file
179
- * @throws Error if download fails or media ID is invalid
180
- *
181
- * @example
182
- * ```typescript
183
- * client.webhooks.handle(req.body, {
184
- * image: async (message, context) => {
185
- * const mediaData = await client.webhooks.downloadMedia(message.image.id);
186
- * // Upload to S3, save to disk, etc.
187
- * await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
188
- * },
189
- * });
190
- * ```
191
- */
192
- async downloadMedia(mediaId: string): Promise<ArrayBuffer> {
193
- if (!mediaId || mediaId.trim().length === 0) {
194
- throw new Error("Media ID is required");
195
- }
196
-
197
- // WhatsApp API endpoint: GET /{version}/{media-id}
198
- // Use HttpClient's getBinary method which handles baseURL, apiVersion, and auth automatically
199
- return this.httpClient.getBinary(`/${mediaId}`);
200
- }
201
-
202
171
  /**
203
172
  * Validate webhook payload structure
204
173
  *
@@ -1,5 +1,5 @@
1
1
  import type { WebhookPayload } from "../../../types/webhooks";
2
- import type { IncomingMessage } from "../../../types/webhooks/incoming-message";
2
+ import type { IncomingMessage } from "../../../types/messages/incoming";
3
3
 
4
4
  /**
5
5
  * Extract all incoming messages from webhook payload
@@ -4,4 +4,5 @@ export type * from "./accounts/index";
4
4
  export type * from "./business/index";
5
5
  export type * from "./templates/index";
6
6
  export type * from "./webhooks/index";
7
+ export type * from "./media/index";
7
8
  export type * from "./debug";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Media metadata response (from GET /MEDIA_ID)
3
+ */
4
+ export type MediaMetadata = {
5
+ messaging_product: "whatsapp";
6
+ url: string;
7
+ mime_type: string;
8
+ sha256: string;
9
+ file_size: string;
10
+ id: string;
11
+ };
12
+