wuzapi 1.1.0 → 1.3.1

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/README.md CHANGED
@@ -285,7 +285,7 @@ await adminClient.admin.deleteUser(2);
285
285
 
286
286
  ### Webhook Module
287
287
 
288
- Configure webhook settings.
288
+ Configure webhook settings and handle incoming WhatsApp events.
289
289
 
290
290
  ```typescript
291
291
  // Set webhook URL
@@ -297,6 +297,919 @@ console.log("Webhook URL:", webhookConfig.webhook);
297
297
  console.log("Subscribed events:", webhookConfig.subscribe);
298
298
  ```
299
299
 
300
+ ## Webhook Event Handling
301
+
302
+ WuzAPI sends real-time events to your webhook endpoint. Here's how to handle different types of events:
303
+
304
+ ## Webhook Payload Structure
305
+
306
+ When S3 is enabled, webhook payloads will include S3 information based on the `media_delivery` setting configured in your WuzAPI instance.
307
+
308
+ ### Standard Event Payload
309
+
310
+ ```json
311
+ {
312
+ "event": {
313
+ "Info": {
314
+ "ID": "3EB06F9067F80BAB89FF",
315
+ "Type": "text",
316
+ "PushName": "John Doe",
317
+ "Timestamp": "2024-12-25T10:30:00Z",
318
+ "Source": {
319
+ "Chat": "5491155553934@s.whatsapp.net",
320
+ "Sender": "5491155553934@s.whatsapp.net",
321
+ "IsFromMe": false,
322
+ "IsGroup": false
323
+ }
324
+ },
325
+ "Message": {
326
+ "conversation": "Hello, this is a test message!"
327
+ },
328
+ "IsEphemeral": false,
329
+ "IsViewOnce": false,
330
+ "IsDocumentWithCaption": false,
331
+ "IsEdit": false
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### S3 Only (`media_delivery: "s3"`)
337
+
338
+ ```json
339
+ {
340
+ "event": {
341
+ "Info": {
342
+ "ID": "3EB06F9067F80BAB89FF",
343
+ "Type": "image",
344
+ "PushName": "John Doe",
345
+ "Timestamp": "2024-12-25T10:30:00Z",
346
+ "Source": {
347
+ "Chat": "5491155553934@s.whatsapp.net",
348
+ "Sender": "5491155553934@s.whatsapp.net",
349
+ "IsFromMe": false,
350
+ "IsGroup": false
351
+ }
352
+ },
353
+ "Message": {
354
+ "imageMessage": {
355
+ "caption": "Check out this photo!",
356
+ "mimetype": "image/jpeg",
357
+ "width": 1920,
358
+ "height": 1080,
359
+ "fileLength": 245632
360
+ }
361
+ },
362
+ "IsEphemeral": false,
363
+ "IsViewOnce": false
364
+ },
365
+ "s3": {
366
+ "url": "https://my-bucket.s3.us-east-1.amazonaws.com/users/abc123/inbox/5491155553934/2024/12/25/images/3EB06F9067F80BAB89FF.jpg",
367
+ "key": "users/abc123/inbox/5491155553934/2024/12/25/images/3EB06F9067F80BAB89FF.jpg",
368
+ "bucket": "my-bucket",
369
+ "size": 245632,
370
+ "mimeType": "image/jpeg",
371
+ "fileName": "3EB06F9067F80BAB89FF.jpg"
372
+ }
373
+ }
374
+ ```
375
+
376
+ ### Both S3 and Base64 (`media_delivery: "both"`)
377
+
378
+ ```json
379
+ {
380
+ "event": {
381
+ "Info": { "..." },
382
+ "Message": {
383
+ "imageMessage": {
384
+ "caption": "Check out this photo!",
385
+ "mimetype": "image/jpeg",
386
+ "width": 1920,
387
+ "height": 1080
388
+ }
389
+ }
390
+ },
391
+ "base64": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=",
392
+ "mimeType": "image/jpeg",
393
+ "fileName": "3EB06F9067F80BAB89FF.jpg",
394
+ "s3": {
395
+ "url": "https://my-bucket.s3.us-east-1.amazonaws.com/users/abc123/inbox/5491155553934/2024/12/25/images/3EB06F9067F80BAB89FF.jpg",
396
+ "key": "users/abc123/inbox/5491155553934/2024/12/25/images/3EB06F9067F80BAB89FF.jpg",
397
+ "bucket": "my-bucket",
398
+ "size": 245632,
399
+ "mimeType": "image/jpeg",
400
+ "fileName": "3EB06F9067F80BAB89FF.jpg"
401
+ }
402
+ }
403
+ ```
404
+
405
+ ### Base64 Only (`media_delivery: "base64"`)
406
+
407
+ ```json
408
+ {
409
+ "event": { "..." },
410
+ "base64": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=",
411
+ "mimeType": "image/jpeg",
412
+ "fileName": "3EB06F9067F80BAB89FF.jpg"
413
+ }
414
+ ```
415
+
416
+ ### Basic Webhook Setup
417
+
418
+ ```typescript
419
+ import express from "express";
420
+ import WuzapiClient, {
421
+ EventType,
422
+ hasS3Media,
423
+ hasBase64Media,
424
+ WebhookPayload,
425
+ } from "wuzapi";
426
+
427
+ const app = express();
428
+ app.use(express.json());
429
+
430
+ const client = new WuzapiClient({
431
+ apiUrl: "http://localhost:8080",
432
+ token: "your-token",
433
+ });
434
+
435
+ // Webhook endpoint with S3 media support
436
+ app.post("/webhook", async (req, res) => {
437
+ try {
438
+ const webhookPayload: WebhookPayload = req.body;
439
+
440
+ // Handle S3 media information if present
441
+ if (hasS3Media(webhookPayload)) {
442
+ console.log("☁️ S3 Media:", {
443
+ url: webhookPayload.s3.url,
444
+ bucket: webhookPayload.s3.bucket,
445
+ size: webhookPayload.s3.size,
446
+ type: webhookPayload.s3.mimeType,
447
+ });
448
+ }
449
+
450
+ if (hasBase64Media(webhookPayload)) {
451
+ console.log("📦 Base64 media included");
452
+ // Process base64 media: webhookPayload.base64
453
+ }
454
+
455
+ // Extract the actual event data
456
+ const event = webhookPayload.event || webhookPayload;
457
+ const eventType = detectEventType(event);
458
+
459
+ // Handle different event types using EventType enum
460
+ switch (eventType) {
461
+ case EventType.MESSAGE:
462
+ await handleMessage(event);
463
+ break;
464
+ case EventType.RECEIPT:
465
+ await handleReceipt(event);
466
+ break;
467
+ case EventType.PRESENCE:
468
+ await handlePresence(event);
469
+ break;
470
+ default:
471
+ console.log(`Unhandled event type: ${eventType}`);
472
+ }
473
+
474
+ res.status(200).json({ success: true });
475
+ } catch (error) {
476
+ console.error("Webhook error:", error);
477
+ res.status(500).json({ error: error.message });
478
+ }
479
+ });
480
+
481
+ // Helper function to detect event type
482
+ function detectEventType(event: any): EventType | string {
483
+ if (event.Message && event.Info) return EventType.MESSAGE;
484
+ if (event.MessageIDs && event.Type) return EventType.RECEIPT;
485
+ if (event.From && typeof event.Unavailable !== "undefined")
486
+ return EventType.PRESENCE;
487
+ if (event.State && event.MessageSource) return EventType.CHAT_PRESENCE;
488
+ if (event.Codes) return EventType.QR;
489
+ // Add more detection logic as needed
490
+ return "Unknown";
491
+ }
492
+
493
+ app.listen(3000, () => {
494
+ console.log("Webhook server running on port 3000");
495
+ });
496
+ ```
497
+
498
+ ### Message Events
499
+
500
+ Handle incoming messages of all types using the comprehensive Message types:
501
+
502
+ ```typescript
503
+ import { getMessageContent } from "wuzapi";
504
+
505
+ async function handleMessage(event) {
506
+ const { Info, Message, IsEphemeral, IsViewOnce } = event;
507
+ const from = Info.RemoteJid.replace("@s.whatsapp.net", "");
508
+ const isGroup = Info.RemoteJid.includes("@g.us");
509
+
510
+ console.log(`New message from ${from}${isGroup ? " (group)" : ""}`);
511
+
512
+ if (IsEphemeral) console.log("⏱️ Ephemeral message");
513
+ if (IsViewOnce) console.log("👁️ View once message");
514
+
515
+ // Use the utility function to get structured message content
516
+ const messageContent = getMessageContent(Message);
517
+
518
+ if (!messageContent) {
519
+ console.log("❓ Unknown message type");
520
+ return;
521
+ }
522
+
523
+ // Handle different message types with full type safety
524
+ switch (messageContent.type) {
525
+ case "text":
526
+ console.log(`💬 Text: ${messageContent.content}`);
527
+
528
+ // Auto-reply to specific messages
529
+ if (messageContent.content.toLowerCase().includes("hello")) {
530
+ await client.chat.sendText({
531
+ Phone: from,
532
+ Body: "👋 Hello! Thanks for your message.",
533
+ });
534
+ }
535
+ break;
536
+
537
+ case "extendedText":
538
+ console.log(`📝 Extended text: ${messageContent.content.text}`);
539
+ if (messageContent.content.canonicalUrl) {
540
+ console.log(`🔗 Link preview: ${messageContent.content.canonicalUrl}`);
541
+ console.log(`📰 Title: ${messageContent.content.title}`);
542
+ }
543
+ break;
544
+
545
+ case "image":
546
+ const imageMsg = messageContent.content;
547
+ console.log(`🖼️ Image: ${imageMsg.caption || "No caption"}`);
548
+ console.log(`📏 Dimensions: ${imageMsg.width}x${imageMsg.height}`);
549
+
550
+ // Download the image with proper types
551
+ if (imageMsg.url && imageMsg.mediaKey) {
552
+ try {
553
+ const media = await client.chat.downloadImage({
554
+ Url: imageMsg.url,
555
+ MediaKey: Array.from(imageMsg.mediaKey),
556
+ Mimetype: imageMsg.mimetype,
557
+ FileSHA256: Array.from(imageMsg.fileSha256),
558
+ FileLength: imageMsg.fileLength,
559
+ });
560
+ console.log("✅ Image downloaded successfully");
561
+ } catch (error) {
562
+ console.log("❌ Failed to download image:", error.message);
563
+ }
564
+ }
565
+ break;
566
+
567
+ case "video":
568
+ const videoMsg = messageContent.content;
569
+ console.log(`🎥 Video: ${videoMsg.caption || "No caption"}`);
570
+ console.log(`⏱️ Duration: ${videoMsg.seconds}s`);
571
+ if (videoMsg.gifPlayback) {
572
+ console.log("🎬 GIF playback enabled");
573
+ }
574
+ break;
575
+
576
+ case "audio":
577
+ const audioMsg = messageContent.content;
578
+ console.log(`🎵 Audio: ${audioMsg.seconds}s`);
579
+ if (audioMsg.ptt) {
580
+ console.log("🎤 Voice note (Push-to-talk)");
581
+ }
582
+ break;
583
+
584
+ case "document":
585
+ const docMsg = messageContent.content;
586
+ console.log(`📄 Document: ${docMsg.fileName || docMsg.title}`);
587
+ console.log(`📊 Size: ${docMsg.fileLength} bytes`);
588
+ console.log(`📋 Type: ${docMsg.mimetype}`);
589
+ break;
590
+
591
+ case "location":
592
+ const locMsg = messageContent.content;
593
+ console.log(
594
+ `📍 Location: ${locMsg.degreesLatitude}, ${locMsg.degreesLongitude}`
595
+ );
596
+ if (locMsg.name) console.log(`🏷️ Name: ${locMsg.name}`);
597
+ if (locMsg.isLiveLocation) console.log("📡 Live location sharing");
598
+ break;
599
+
600
+ case "contact":
601
+ console.log(`👤 Contact: ${messageContent.content.displayName}`);
602
+ break;
603
+
604
+ case "sticker":
605
+ const stickerMsg = messageContent.content;
606
+ console.log(`😀 Sticker`);
607
+ if (stickerMsg.isAnimated) console.log("🎬 Animated");
608
+ if (stickerMsg.isLottie) console.log("🎨 Lottie");
609
+ break;
610
+
611
+ case "buttons":
612
+ const btnMsg = messageContent.content;
613
+ console.log(`🔘 Interactive buttons: ${btnMsg.contentText}`);
614
+ console.log(`🔢 ${btnMsg.buttons?.length || 0} buttons`);
615
+ break;
616
+
617
+ case "list":
618
+ const listMsg = messageContent.content;
619
+ console.log(`📋 List: ${listMsg.title}`);
620
+ console.log(`📝 ${listMsg.sections?.length || 0} sections`);
621
+ break;
622
+
623
+ case "buttonsResponse":
624
+ const btnResponse = messageContent.content;
625
+ console.log(`✅ Button clicked: ${btnResponse.selectedDisplayText}`);
626
+ console.log(`🆔 Button ID: ${btnResponse.selectedButtonId}`);
627
+
628
+ // Handle button responses
629
+ switch (btnResponse.selectedButtonId) {
630
+ case "help":
631
+ await client.chat.sendText({
632
+ Phone: from,
633
+ Body: "🆘 How can I help you?",
634
+ });
635
+ break;
636
+ case "info":
637
+ await client.chat.sendText({
638
+ Phone: from,
639
+ Body: "ℹ️ Here's some information...",
640
+ });
641
+ break;
642
+ }
643
+ break;
644
+
645
+ case "listResponse":
646
+ const listResponse = messageContent.content;
647
+ console.log(`✅ List selection: ${listResponse.title}`);
648
+ if (listResponse.singleSelectReply) {
649
+ console.log(
650
+ `🆔 Selected: ${listResponse.singleSelectReply.selectedRowId}`
651
+ );
652
+ }
653
+ break;
654
+
655
+ case "poll":
656
+ const pollMsg = messageContent.content;
657
+ console.log(`📊 Poll: ${pollMsg.name}`);
658
+ console.log(`🔢 Options: ${pollMsg.options?.length || 0}`);
659
+ break;
660
+
661
+ case "reaction":
662
+ const reactionMsg = messageContent.content;
663
+ console.log(`😊 Reaction: ${reactionMsg.text}`);
664
+ console.log(`📝 To message: ${reactionMsg.key?.id}`);
665
+ break;
666
+
667
+ case "groupInvite":
668
+ const inviteMsg = messageContent.content;
669
+ console.log(`👥 Group invite: ${inviteMsg.groupName}`);
670
+ console.log(`🔗 Code: ${inviteMsg.inviteCode}`);
671
+ break;
672
+
673
+ default:
674
+ console.log(`❓ Unhandled message type: ${messageContent.type}`);
675
+ }
676
+ }
677
+ ```
678
+
679
+ ### Read Receipts and Delivery Confirmations
680
+
681
+ Handle message delivery and read confirmations:
682
+
683
+ ```typescript
684
+ async function handleReceipt(event) {
685
+ const { MessageSource, MessageIDs, Type, Timestamp } = event;
686
+ const from = MessageSource.Chat.User;
687
+
688
+ console.log(
689
+ `Receipt from ${from}: ${Type} for ${MessageIDs.length} message(s)`
690
+ );
691
+
692
+ switch (Type) {
693
+ case "delivery":
694
+ console.log(`✅ Messages delivered to ${from}`);
695
+ // Update your database to mark messages as delivered
696
+ break;
697
+
698
+ case "read":
699
+ console.log(`👀 Messages read by ${from}`);
700
+ // Update your database to mark messages as read
701
+ break;
702
+
703
+ case "played":
704
+ console.log(`▶️ Voice/video messages played by ${from}`);
705
+ // Update your database to mark media as played
706
+ break;
707
+ }
708
+ }
709
+ ```
710
+
711
+ ### Presence and Typing Indicators
712
+
713
+ Handle user online/offline status and typing indicators:
714
+
715
+ ```typescript
716
+ // User online/offline status
717
+ async function handlePresence(event) {
718
+ const { From, Unavailable, LastSeen } = event;
719
+ const user = From.User;
720
+
721
+ if (Unavailable) {
722
+ console.log(`🔴 ${user} went offline (last seen: ${LastSeen})`);
723
+ } else {
724
+ console.log(`🟢 ${user} is online`);
725
+ }
726
+ }
727
+
728
+ // Typing indicators
729
+ async function handleChatPresence(event) {
730
+ const { MessageSource, State, Media } = event;
731
+ const from = MessageSource.Sender.User;
732
+ const isGroup = MessageSource.IsGroup;
733
+
734
+ if (State === "composing") {
735
+ if (Media === "text") {
736
+ console.log(`⌨️ ${from} is typing${isGroup ? " in group" : ""}...`);
737
+ } else {
738
+ console.log(
739
+ `📎 ${from} is sending ${Media}${isGroup ? " in group" : ""}...`
740
+ );
741
+ }
742
+ } else if (State === "paused") {
743
+ console.log(`⏸️ ${from} stopped typing`);
744
+ }
745
+ }
746
+ ```
747
+
748
+ ### Group Events
749
+
750
+ Handle group-related events:
751
+
752
+ ```typescript
753
+ // Group info updates
754
+ async function handleGroupInfo(event) {
755
+ const { JID, GroupName, Participants, Sender } = event;
756
+ console.log(`👥 Group info updated for ${GroupName} (${JID.User})`);
757
+ console.log(`👤 Updated by: ${Sender.User}`);
758
+ console.log(`👥 Participants: ${Participants.length}`);
759
+ }
760
+
761
+ // When you're added to a group
762
+ async function handleJoinedGroup(event) {
763
+ const { Reason, Type, Participants } = event;
764
+ console.log(`🎉 Joined group! Reason: ${Reason}, Type: ${Type}`);
765
+ console.log(`👥 Group has ${Participants.length} participants`);
766
+
767
+ // Send welcome message
768
+ // Note: You'll need the group JID from the event context
769
+ }
770
+ ```
771
+
772
+ ### Connection Events
773
+
774
+ Handle connection status changes:
775
+
776
+ ```typescript
777
+ async function handleConnected(event) {
778
+ console.log("🟢 Connected to WhatsApp!");
779
+ // Your bot is now ready to send messages
780
+ }
781
+
782
+ async function handleDisconnected(event) {
783
+ console.log("🔴 Disconnected from WhatsApp");
784
+ // Attempt to reconnect or notify administrators
785
+ }
786
+
787
+ async function handleLoggedOut(event) {
788
+ const { Reason, OnConnect } = event;
789
+ console.log(`🚪 Logged out from WhatsApp. Reason: ${Reason}`);
790
+
791
+ if (OnConnect) {
792
+ console.log("⚠️ Logout occurred during connection");
793
+ }
794
+
795
+ // You may need to scan QR code again
796
+ }
797
+ ```
798
+
799
+ ### QR Code Events
800
+
801
+ Handle QR code generation for initial setup:
802
+
803
+ ```typescript
804
+ async function handleQR(event) {
805
+ const { Codes } = event;
806
+ console.log("📱 New QR codes received:");
807
+
808
+ Codes.forEach((code, index) => {
809
+ console.log(`📷 QR Code ${index + 1}: ${code}`);
810
+ // Display QR code to user or save to file
811
+ });
812
+ }
813
+ ```
814
+
815
+ ### Profile and Contact Updates
816
+
817
+ Handle profile picture and contact information changes:
818
+
819
+ ```typescript
820
+ // Profile picture changes
821
+ async function handlePicture(event) {
822
+ const { JID, Author, Remove, Timestamp } = event;
823
+ const target = JID.User;
824
+ const changer = Author.User;
825
+
826
+ if (Remove) {
827
+ console.log(`🗑️ ${changer} removed profile picture for ${target}`);
828
+ } else {
829
+ console.log(`🖼️ ${changer} updated profile picture for ${target}`);
830
+ }
831
+ }
832
+
833
+ // Contact info updates
834
+ async function handleContact(event) {
835
+ const { JID, Found, FullName, PushName, BusinessName } = event;
836
+ console.log(`👤 Contact info: ${FullName || PushName} (${JID.User})`);
837
+
838
+ if (BusinessName) {
839
+ console.log(`🏢 Business: ${BusinessName}`);
840
+ }
841
+
842
+ console.log(`✅ Found in WhatsApp: ${Found}`);
843
+ }
844
+
845
+ // Name changes
846
+ async function handlePushName(event) {
847
+ const { JID, OldPushName, NewPushName } = event;
848
+ console.log(
849
+ `📝 ${JID.User} changed name from "${OldPushName}" to "${NewPushName}"`
850
+ );
851
+ }
852
+ ```
853
+
854
+ ### Error Handling
855
+
856
+ Handle various error events:
857
+
858
+ ```typescript
859
+ // Undecryptable messages
860
+ async function handleUndecryptableMessage(event) {
861
+ const { Info, IsUnavailable, UnavailableType, DecryptFailMode } = event;
862
+ console.log(`❌ Failed to decrypt message from ${Info.Source.Sender.User}`);
863
+ console.log(
864
+ `📊 Unavailable: ${IsUnavailable}, Type: ${UnavailableType}, Mode: ${DecryptFailMode}`
865
+ );
866
+
867
+ // Log for debugging or request message retry
868
+ }
869
+
870
+ // Stream errors
871
+ async function handleStreamError(event) {
872
+ const { Code } = event;
873
+ console.log(`🚨 Stream error: ${Code}`);
874
+
875
+ // Handle specific error codes
876
+ switch (Code) {
877
+ case "conflict":
878
+ console.log("Another client connected with same credentials");
879
+ break;
880
+ case "stream:error":
881
+ console.log("General stream error occurred");
882
+ break;
883
+ default:
884
+ console.log("Unknown stream error");
885
+ }
886
+ }
887
+ ```
888
+
889
+ ### Complete Webhook Server Example
890
+
891
+ Here's a complete webhook server that handles all event types:
892
+
893
+ ```typescript
894
+ import express from "express";
895
+ import WuzapiClient, {
896
+ Message,
897
+ Receipt,
898
+ Presence,
899
+ EventGroupInfo,
900
+ } from "wuzapi";
901
+
902
+ const app = express();
903
+ app.use(express.json());
904
+
905
+ const client = new WuzapiClient({
906
+ apiUrl: process.env.WUZAPI_URL || "http://localhost:8080",
907
+ token: process.env.WUZAPI_TOKEN || "your-token",
908
+ });
909
+
910
+ // Event router
911
+ const eventHandlers = {
912
+ Message: handleMessage,
913
+ Receipt: handleReceipt,
914
+ Presence: handlePresence,
915
+ ChatPresence: handleChatPresence,
916
+ GroupInfo: handleGroupInfo,
917
+ JoinedGroup: handleJoinedGroup,
918
+ Connected: handleConnected,
919
+ Disconnected: handleDisconnected,
920
+ LoggedOut: handleLoggedOut,
921
+ QR: handleQR,
922
+ Picture: handlePicture,
923
+ Contact: handleContact,
924
+ PushName: handlePushName,
925
+ UndecryptableMessage: handleUndecryptableMessage,
926
+ StreamError: handleStreamError,
927
+ };
928
+
929
+ app.post("/webhook", async (req, res) => {
930
+ try {
931
+ const eventData = req.body;
932
+
933
+ // Log all events for debugging
934
+ console.log("📨 Webhook received:", JSON.stringify(eventData, null, 2));
935
+
936
+ // Route to appropriate handler based on event structure
937
+ for (const [eventType, handler] of Object.entries(eventHandlers)) {
938
+ if (isEventType(eventData, eventType)) {
939
+ await handler(eventData);
940
+ break;
941
+ }
942
+ }
943
+
944
+ res.status(200).json({ success: true });
945
+ } catch (error) {
946
+ console.error("❌ Webhook processing error:", error);
947
+ res.status(500).json({ error: error.message });
948
+ }
949
+ });
950
+
951
+ // Helper function to identify event types
952
+ function isEventType(event, type) {
953
+ switch (type) {
954
+ case "Message":
955
+ return event.Message && event.Info;
956
+ case "Receipt":
957
+ return event.MessageIDs && event.Type;
958
+ case "Presence":
959
+ return event.From && typeof event.Unavailable !== "undefined";
960
+ case "ChatPresence":
961
+ return event.State && event.MessageSource;
962
+ case "GroupInfo":
963
+ return event.GroupName && event.Participants;
964
+ case "QR":
965
+ return event.Codes;
966
+ // Add more type checks as needed
967
+ default:
968
+ return false;
969
+ }
970
+ }
971
+
972
+ // Health check
973
+ app.get("/health", (req, res) => {
974
+ res.json({ status: "healthy", timestamp: new Date().toISOString() });
975
+ });
976
+
977
+ // Initialize
978
+ async function initialize() {
979
+ try {
980
+ // Connect to WhatsApp with all events using EventType enum
981
+ await client.session.connect({
982
+ Subscribe: [
983
+ EventType.MESSAGE,
984
+ EventType.RECEIPT,
985
+ EventType.PRESENCE,
986
+ EventType.CHAT_PRESENCE,
987
+ EventType.GROUP_INFO,
988
+ EventType.CONTACT,
989
+ EventType.PUSH_NAME,
990
+ EventType.PICTURE,
991
+ EventType.QR,
992
+ EventType.CONNECTED,
993
+ EventType.DISCONNECTED,
994
+ EventType.LOGGED_OUT,
995
+ EventType.UNDECRYPTABLE_MESSAGE,
996
+ EventType.STREAM_ERROR,
997
+ ],
998
+ Immediate: false,
999
+ });
1000
+
1001
+ // Set webhook
1002
+ const webhookUrl =
1003
+ process.env.WEBHOOK_URL || "http://localhost:3000/webhook";
1004
+ await client.webhook.setWebhook(webhookUrl);
1005
+
1006
+ app.listen(3000, () => {
1007
+ console.log("🚀 Webhook server running on port 3000");
1008
+ console.log("🎉 Ready to receive WhatsApp events!");
1009
+ });
1010
+ } catch (error) {
1011
+ console.error("❌ Initialization failed:", error);
1012
+ process.exit(1);
1013
+ }
1014
+ }
1015
+
1016
+ initialize();
1017
+ ```
1018
+
1019
+ ### Event Types Reference
1020
+
1021
+ All webhook events are fully typed. Import the specific event types you need:
1022
+
1023
+ ```typescript
1024
+ import {
1025
+ // Event type enum for all possible events
1026
+ EventType,
1027
+
1028
+ // Core message types
1029
+ Message,
1030
+ MessageEvent,
1031
+ MessageContent,
1032
+ getMessageContent,
1033
+
1034
+ // Specific message types
1035
+ ImageMessage,
1036
+ VideoMessage,
1037
+ AudioMessage,
1038
+ DocumentMessage,
1039
+ LocationMessage,
1040
+ ContactMessage,
1041
+ StickerMessage,
1042
+ ButtonsMessage,
1043
+ ListMessage,
1044
+ PollCreationMessage,
1045
+ ReactionMessage,
1046
+
1047
+ // Webhook payload types
1048
+ WebhookPayload,
1049
+ S3MediaInfo,
1050
+ S3OnlyWebhookPayload,
1051
+ Base64OnlyWebhookPayload,
1052
+ BothMediaWebhookPayload,
1053
+ hasS3Media,
1054
+ hasBase64Media,
1055
+ hasBothMedia,
1056
+
1057
+ // Event types
1058
+ Receipt,
1059
+ Presence,
1060
+ ChatPresence,
1061
+ EventGroupInfo,
1062
+ EventContact,
1063
+ QR,
1064
+ Picture,
1065
+ PushName,
1066
+ Connected,
1067
+ Disconnected,
1068
+ LoggedOut,
1069
+ UndecryptableMessage,
1070
+ StreamError,
1071
+ WhatsAppEvent,
1072
+ } from "wuzapi";
1073
+
1074
+ // Type-safe event handling with EventType enum
1075
+ async function handleTypedWebhook(webhookPayload: WebhookPayload) {
1076
+ const event = webhookPayload.event;
1077
+
1078
+ // Type-safe event detection using EventType enum
1079
+ const eventType = detectEventType(event);
1080
+
1081
+ switch (eventType) {
1082
+ case EventType.MESSAGE:
1083
+ const messageEvent = event as MessageEvent;
1084
+ const messageContent = getMessageContent(messageEvent.Message);
1085
+
1086
+ if (messageContent?.type === "image") {
1087
+ // TypeScript knows this is an ImageMessage
1088
+ const imageMsg: ImageMessage = messageContent.content;
1089
+ console.log(`Image dimensions: ${imageMsg.width}x${imageMsg.height}`);
1090
+
1091
+ // Handle S3 media if available
1092
+ if (hasS3Media(webhookPayload)) {
1093
+ console.log(`S3 URL: ${webhookPayload.s3.url}`);
1094
+ // Download from S3 or process as needed
1095
+ }
1096
+
1097
+ // Handle Base64 media if available
1098
+ if (hasBase64Media(webhookPayload)) {
1099
+ console.log("Processing base64 image data");
1100
+ // Process base64 data: webhookPayload.base64
1101
+ }
1102
+ }
1103
+ break;
1104
+
1105
+ case EventType.RECEIPT:
1106
+ const receiptEvent = event as Receipt;
1107
+ console.log(`Receipt type: ${receiptEvent.Type}`);
1108
+ break;
1109
+
1110
+ case EventType.PRESENCE:
1111
+ const presenceEvent = event as Presence;
1112
+ console.log(`User ${presenceEvent.From.User} presence update`);
1113
+ break;
1114
+
1115
+ default:
1116
+ console.log(`Unhandled event: ${eventType}`);
1117
+ }
1118
+ }
1119
+
1120
+ // Complete webhook server with S3 support
1121
+ app.post("/webhook", async (req, res) => {
1122
+ try {
1123
+ const payload: WebhookPayload = req.body;
1124
+ await handleTypedWebhook(payload);
1125
+ res.status(200).json({ success: true });
1126
+ } catch (error) {
1127
+ console.error("Webhook error:", error);
1128
+ res.status(500).json({ error: error.message });
1129
+ }
1130
+ });
1131
+
1132
+ // Helper function to handle all message types generically
1133
+ function processMessageContent(content: MessageContent) {
1134
+ switch (content.type) {
1135
+ case "text":
1136
+ // content.content is string
1137
+ return content.content.toUpperCase();
1138
+
1139
+ case "image":
1140
+ // content.content is ImageMessage
1141
+ return `Image: ${content.content.caption || "No caption"}`;
1142
+
1143
+ case "video":
1144
+ // content.content is VideoMessage
1145
+ return `Video (${content.content.seconds}s): ${
1146
+ content.content.caption || "No caption"
1147
+ }`;
1148
+
1149
+ // TypeScript ensures you handle all possible types
1150
+ default:
1151
+ return `Unknown message type: ${content.type}`;
1152
+ }
1153
+ }
1154
+ ```
1155
+
1156
+ ### Advanced Message Type Handling
1157
+
1158
+ For complex message processing, you can work directly with the typed message structures:
1159
+
1160
+ ```typescript
1161
+ import {
1162
+ Message,
1163
+ ExtendedTextMessage,
1164
+ ButtonsMessage,
1165
+ ContextInfo,
1166
+ } from "wuzapi";
1167
+
1168
+ // Check for extended text with link preview
1169
+ function hasLinkPreview(message: Message): boolean {
1170
+ return !!message.extendedTextMessage?.canonicalUrl;
1171
+ }
1172
+
1173
+ // Extract context info from any message
1174
+ function getMessageContext(message: Message): ContextInfo | undefined {
1175
+ // Check various message types for context info
1176
+ return (
1177
+ message.extendedTextMessage?.contextInfo ||
1178
+ message.imageMessage?.contextInfo ||
1179
+ message.videoMessage?.contextInfo ||
1180
+ message.audioMessage?.contextInfo
1181
+ );
1182
+ }
1183
+
1184
+ // Process interactive messages
1185
+ function handleInteractiveMessage(message: Message) {
1186
+ if (message.buttonsMessage) {
1187
+ const btns = message.buttonsMessage;
1188
+ console.log(`Interactive message: ${btns.contentText}`);
1189
+
1190
+ btns.buttons?.forEach((button) => {
1191
+ if (button.type === ButtonType.RESPONSE) {
1192
+ console.log(`Response button: ${button.buttonText?.displayText}`);
1193
+ } else if (button.type === ButtonType.NATIVE_FLOW) {
1194
+ console.log(`Native flow: ${button.nativeFlowInfo?.name}`);
1195
+ }
1196
+ });
1197
+ }
1198
+
1199
+ if (message.listMessage) {
1200
+ const list = message.listMessage;
1201
+ console.log(`List: ${list.title}`);
1202
+
1203
+ list.sections?.forEach((section) => {
1204
+ console.log(`Section: ${section.title}`);
1205
+ section.rows?.forEach((row) => {
1206
+ console.log(`- ${row.title}: ${row.description}`);
1207
+ });
1208
+ });
1209
+ }
1210
+ }
1211
+ ```
1212
+
300
1213
  ## Error Handling
301
1214
 
302
1215
  The library provides comprehensive error handling with detailed error information:
@@ -401,6 +1314,12 @@ if (isConnected) {
401
1314
 
402
1315
  ## Examples
403
1316
 
1317
+ For complete working examples, check the `examples/` directory:
1318
+
1319
+ - **`basic-usage.js`** - Basic client setup and usage
1320
+ - **`chatbot-example.js`** - Simple chatbot with command handling
1321
+ - **`webhook-events-example.js`** - Comprehensive webhook event handling
1322
+
404
1323
  ### Complete Chat Bot Example
405
1324
 
406
1325
  ```typescript