Chat about this codebase

AI-powered code exploration

Online

1. Project Overview

WPPConnect is an open-source Node.js library that automates and extends WhatsApp Web. It simplifies messaging workflows, supports multiple sessions, handles rich media, and exposes Business API–style features for developers building chatbots, CRMs, and analytics tools.

1.1 Key Capabilities

  • WhatsApp Web Automation
    • Send and receive messages, react to events, manage chats
    • Automate contact retrieval, group management, labels
  • Multi-Session Support
    • Run multiple WhatsApp accounts in parallel
    • Isolated session data per client
  • Rich Media Handling
    • Send images, audio, video, documents, stickers
    • Download and process incoming attachments
  • Business API–Style Features
    • Template message support
    • Message scheduling and retries
    • Session lifecycle hooks and status monitoring

1.2 When to Use WPPConnect

  • Build chatbots or customer-service integrations on WhatsApp
  • Integrate WhatsApp messaging into web or desktop apps
  • Automate marketing workflows, notifications, or alerts
  • Manage multiple WhatsApp accounts from a single Node.js process

1.3 Technical Details

  • Supported Node.js versions: 12.x, 14.x, 16.x (LTS)
  • License: GNU Lesser General Public License v3 (LGPL-3.0)
  • Funding: Community-driven; contributions and sponsorships welcome

1.4 High-Level Feature List

  • Real-time message events (onMessage, onAck, onParticipantsChanged)
  • Chat and contact CRUD operations
  • Group creation, invitation, and role management
  • Media upload/download with progress callbacks
  • Session management: create, list, kill sessions
  • Webhook and plugin hooks for custom logic
  • Built-in rate-limit handling and reconnect strategies

Start by installing the package and initializing a session:

npm install @wppconnect-team/wppconnect
const wppconnect = require('@wppconnect-team/wppconnect');

wppconnect.create({
  session: 'my-session',            // arbitrary session name
  headless: true,                   // run in headless Chrome
  puppeteerOptions: { args: ['--no-sandbox'] }
})
.then(client => {
  client.onMessage(async message => {
    console.log('Received:', message.body);
    await client.sendText(message.from, 'Hello from WPPConnect!');
  });
})
.catch(err => console.error('Init error:', err));
## 2. Quick Start

Get up and running in minutes: install the package, satisfy browser prerequisites, spin up your first client, scan the QR code, and send your first message.

### 2.1 Installation

Install the latest stable release:

  npm install @wppconnect-team/wppconnect

Or with Yarn:

  yarn add @wppconnect-team/wppconnect

To try the cutting-edge nightly build:

  npm install @wppconnect-team/wppconnect@nightly  
  // or  
  yarn add @wppconnect-team/wppconnect@nightly

### 2.2 Browser & System Prerequisites

• Node.js 14+  
• Chromium or Chrome installed (v100+ recommended)  
• Optional: ffmpeg on PATH for media encoding/decoding  

If Puppeteer cannot find your browser, set the `PUPPETEER_EXECUTABLE_PATH` environment variable:

  export PUPPETEER_EXECUTABLE_PATH="/path/to/chrome"

### 2.3 First WhatsApp Message

Create a new file `index.js`:

```javascript
// index.js
const { create } = require('@wppconnect-team/wppconnect');

async function start() {
  // Initialize client with a custom session name
  const client = await create({
    session: 'my-first-session',
    headless: true,           // set to false to watch the browser
    qrTimeout: 0,             // disable QR timeout
  });

  // Send a text message to a contact
  const chatId = '5511999999999@c.us'; // Brazil example: country code + number + '@c.us'
  await client.sendText(chatId, 'Hello from WPPConnect!');
  console.log('✅ Message sent to', chatId);

  // Optionally: close client when done
  await client.close();
}

start().catch(console.error);

Run:

node index.js

2.4 QR Code Flow & Troubleshooting

• On first run, the terminal prints an ASCII QR code. Scan it with your phone’s WhatsApp “Linked Devices” > “Link a Device.”
• If you don’t see a QR:
– Ensure headless: false to display the browser window.
– Verify your Chrome/Chromium path or PUPPETEER_EXECUTABLE_PATH.
– Check network/firewall rules blocking web.whatsapp.com.
• Common states emitted by the client:

client.onStateChanged(state => console.log('⚙️ State:', state));
// e.g., 'PAIRING', 'CONNECTED', 'DISCONNECTED'

• If authentication fails continuously, delete the session folder under your project directory and restart.

2.5 Where to Ask for Help

• GitHub Issues: https://github.com/wppconnect-team/wppconnect/issues
• Discussions & feature requests: https://github.com/wppconnect-team/wppconnect/discussions
• Community Chat (Discord invite on repo README)
• Stack Overflow: Tag your question wppconnect

Welcome aboard! You’ve just sent your first WhatsApp message via WPPConnect.

3. Core Concepts

Fundamental ideas and configuration points for any serious use of WPPConnect. Each subsection explains one core feature of the library.

Scraping WhatsApp QR Code Image

Purpose: Retrieve the current WhatsApp Web QR code as a base64 image and its raw URL for custom handling (UI display, disk saving, ASCII generation).

Description
The scrapeImg helper automates DOM queries to locate the <canvas> element containing the QR code and its parent [data-ref] URL attribute. It will:

  • Click the “reload” button if present to force a fresh QR.
  • Wait until the new data-ref attribute populates.
  • Extract: • base64Image: the canvas image as a Data URL
    urlCode: the raw QR URL string

Function signature

import { Page } from 'puppeteer';
import { ScrapQrcode } from '../model/qrcode';

async function scrapeImg(page: Page): Promise<ScrapQrcode | undefined>

ScrapQrcode shape

  • base64Image: string
  • urlCode: string

Code example: standalone usage

import puppeteer from 'puppeteer';
import { scrapeImg } from 'wppconnect/api/helpers/scrape-img-qr';

async function fetchQr() {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('https://web.whatsapp.com');
  // Wait until the QR canvas appears
  await page.waitForSelector('canvas');

  const qrData = await scrapeImg(page);
  if (qrData) {
    console.log('QR URL:', qrData.urlCode);
    // Strip prefix if needed
    const base64 = qrData.base64Image.split(',')[1];
    require('fs').writeFileSync('qr.png', base64, 'base64');
  } else {
    console.error('Failed to scrape QR code');
  }

  await browser.close();
}

Integration in HostLayer
The HostLayer.getQrCode() method simply calls scrapeImg. Override the catchQR callback to receive QR data on each scan request:

import { create } from 'wppconnect';
import { ScrapQrcode } from 'wppconnect/api/model/qrcode';

create({
  session: 'mySession',
  catchQR: (base64Image, asciiQr, attempt, urlCode) => {
    console.log(`Attempt ${attempt}: ${urlCode}`);
    // Display asciiQr in terminal or UI
  }
}).then(client => {
  // client is ready once logged in
});

Practical guidance

  • Always call page.waitForSelector('canvas') before scrapeImg.
  • To generate ASCII QR codes, use asciiQr(result.urlCode).
  • Handle undefined returns when the DOM structure changes.
  • scrapeImg clicks the reload button under the same [data-ref] container.
  • Use HostLayer.waitForQrCodeScan() after start() to block until the scan completes.

FileTokenStore

Purpose: Persist session tokens on the filesystem with customizable encoding, decoding, directory, and extension.

Instantiation and Options

By default, FileTokenStore serializes tokens as JSON under ./tokens/*.data.json:

import { FileTokenStore } from '@wppconnect-team/wppconnect/token-store';

const store = new FileTokenStore();
// default options:
// {
//   decodeFunction: JSON.parse,
//   encodeFunction: JSON.stringify,
//   encoding: 'utf8',
//   fileExtension: '.data.json',
//   path: './tokens',
// }

Customize directory, extension, or serialization:

import YAML from 'yaml';

const customStore = new FileTokenStore({
  path: './my_tokens',
  fileExtension: '.session.json',
  encoding: 'utf16',
  decodeFunction: txt => YAML.parse(txt),
  encodeFunction: obj => YAML.stringify(obj),
});

API Methods

  1. resolverPath(sessionName: string): string
    Returns an absolute path for the token file.

  2. getToken(sessionName: string): Promise<SessionToken | undefined>
    Reads and decodes the token or returns undefined if missing or invalid.

  3. setToken(sessionName: string, tokenData: SessionToken | null): Promise<boolean>
    Validates via isValidSessionToken, creates directories, writes encoded data.

  4. removeToken(sessionName: string): Promise<boolean>
    Deletes the token file; returns true if removed.

  5. listTokens(): Promise<string[]>
    Lists files by extension and returns session names without extensions.

Practical Usage

import { FileTokenStore } from '@wppconnect-team/wppconnect/token-store';
import { create } from '@wppconnect-team/wppconnect';

const tokenStore = new FileTokenStore({ path: './sessions' });

// Initialize a session with file storage
await create({ session: 'userA', tokenStore });

// Later, retrieve and inspect the token
const token = await tokenStore.getToken('userA');
console.log('Loaded token for userA:', token);

// List active sessions
const sessions = await tokenStore.listTokens();
console.log('Active sessions:', sessions);

// Remove a session on logout
const removed = await tokenStore.removeToken('userA');
console.log('Session userA removed?', removed);

Tips and Gotchas

  • Corrupted token files are ignored and treated as missing.
  • Always await setToken and check its boolean return.
  • Custom encodeFunction must return a string.
  • Nested directory creation uses fs.promises.mkdir(..., { recursive: true }).

Controls Layer

Purpose: Manage contacts, chats, messages, group settings, and Web session limits.

Blocklist

  • blockContact(contactId: string): Promise<boolean>
  • unblockContact(contactId: string): Promise<boolean>

Example

await client.blockContact('1234567890@c.us');
await client.unblockContact('1234567890@c.us');

Chat Management

  • markUnseenMessage(chatId: string): Promise<boolean>
  • deleteChat(chatId: string): Promise<boolean>
  • archiveChat(chatId: string, option = true): Promise<boolean>
  • pinChat(chatId: string, option: boolean, nonExistent?: boolean): Promise<object>
  • clearChat(chatId: string, keepStarred = true): Promise<boolean>

Example

await client.markUnseenMessage('123@c.us');
const ok = await client.deleteChat('123@c.us');
if (ok) console.log('Chat deleted');
await client.archiveChat('123@c.us', true);
await client.pinChat('123@c.us', true, true);
await client.clearChat('123@c.us');

Message Operations

  • deleteMessage(chatId: string, messageId: string[]|string, onlyLocal?: boolean, deleteMediaInDevice?: boolean): Promise<boolean>
  • editMessage(msgId: string, newText: string, options?): Promise<Message>
  • starMessage(messageId: string[]|string, star?: boolean): Promise<number>

Example

await client.deleteMessage('123@c.us', 'true_123@c.us_ABC', true);
const edited = await client.editMessage('true_123@c.us_ABC', 'Updated text');
console.log('New body:', edited.body);
await client.starMessage('true_123@c.us_ABC', true);

Group Settings

  • setMessagesAdminsOnly(chatId: string, option: boolean): Promise<boolean>
  • setTemporaryMessages(chatOrGroupId: string, value: boolean): Promise<boolean>

Example

await client.setMessagesAdminsOnly('999999@g.us', true);
await client.setTemporaryMessages('123@c.us', true);

Connection Limits

  • setLimit(key: 'maxMediaSize' | 'maxFileSize' | 'maxShare' | 'statusVideoMaxDuration' | 'unlimitedPin', value: any): Promise<any>

Example

await client.setLimit('maxMediaSize', 50 * 1024 * 1024); // 50MB
await client.setLimit('unlimitedPin', true);

Practical Tips

  • Check boolean or object returns for success.
  • Use nonExistent=true in pinChat to auto-create missing chats.
  • Temporary messages apply to individual and group chats.
  • Adjust limits sparingly to avoid Web instability.

Event Registration with ListenerLayer

Purpose: Subscribe to real-time WhatsApp events using the ListenerLayer API and dispose listeners when done.

1. Core Mechanism: registerEvent

ListenerLayer wraps a Node.js EventEmitter to forward browser‐side WAPI events.

protected registerEvent(event: string|symbol, listener: (...args:any[])=>void) {
  this.log('debug', `Registering ${event.toString()} event`);
  this.listenerEmitter.on(event, listener);
  return { dispose: () => this.listenerEmitter.off(event, listener) };
}

2. Commonly Used Event Methods

  • onMessage(callback: Message) – incoming messages only.
  • onAnyMessage(callback: Message) – all new messages.
  • onNotificationMessage(callback: Message) – system notifications.
  • onAck(callback: Ack) – delivery/read acknowledgements.
  • onIncomingCall(callback: IncomingCall) – incoming calls.
  • onPresenceChanged(id?, callback: PresenceEvent) – presence updates (requires subscribePresence).
  • onLiveLocation(id?, callback: LiveLocation) – live‐location updates.

Each returns { dispose(): void } to stop listening.

3. Usage Examples

import { Client } from 'wppconnect';

const client = await Client.create();
const listener = client.getListener();

// Incoming messages
const msgHandle = listener.onMessage(msg => {
  console.log('New incoming message:', msg);
});

// All messages
const anyHandle = listener.onAnyMessage(msg => {
  console.log('Message event:', msg.from, msg.id);
});

// Presence for specific contacts
await listener.subscribePresence(['1234@c.us','5678@c.us']);
const presHandle = listener.onPresenceChanged(['1234@c.us'], presence => {
  console.log(presence.id, 'is now', presence.type);
});

// Live location in a group
const liveHandle = listener.onLiveLocation('99977-111@g.us', loc => {
  console.log('Live location update:', loc);
});

// Cleanup
msgHandle.dispose();
anyHandle.dispose();
presHandle.dispose();
liveHandle.dispose();

4. Practical Tips

  • Always call dispose() on handles to avoid memory leaks.
  • Call subscribePresence before onPresenceChanged.
  • Use onAnyMessage for complete message logging.
  • Combine onInterfaceChange and onStateChange to monitor UI and connection.

Custom Log Labels (Session and Type)

Purpose: Prefix log messages with [session:type] metadata using the formatLabelSession transformer.

How It Works

In src/utils/logger.ts:

export const formatLabelSession: FormatWrap = format((info: SessionInfo) => {
  const parts: string[] = [];
  if (info.session) { parts.push(info.session); delete info.session; }
  if (info.type)    { parts.push(info.type);    delete info.type; }
  if (parts.length) {
    info.message = `[${parts.join(':')}] ${info.message}`;
  }
  return info;
});

The default logger combines this with colorizing and padding:

export const defaultLogger = createLogger({
  level: 'silly',
  levels: config.npm.levels,
  format: format.combine(
    formatLabelSession(),
    format.colorize(),
    format.padLevels(),
    format.simple()
  ),
  transports: [new transports.Console()],
});

Usage Examples

const wppconnect = require('@wppconnect-team/wppconnect');

// Prints: info: [mySession:connect] Client connected successfully
wppconnect.defaultLogger.info('Client connected successfully', {
  session: 'mySession',
  type: 'connect'
});

// Prefix [mySession]
wppconnect.defaultLogger.warn('Reconnecting...', {
  session: 'mySession'
});

// Prefix [:cleanup]
wppconnect.defaultLogger.debug('Cleaning up resources', {
  type: 'cleanup'
});

Custom Winston logger:

const winston = require('winston');
const { formatLabelSession } = require('@wppconnect-team/wppconnect/src/utils/logger');

const customLogger = winston.createLogger({
  level: 'debug',
  format: winston.format.combine(
    formatLabelSession(),
    winston.format.json()
  ),
  transports: [new winston.transports.Console()]
});

customLogger.info('Initializing', { session: 'svc123', type: 'init' });

Practical Tips

  • Always include session and/or type in log metadata.
  • If you remove formatLabelSession() from the chain, metadata fields log as JSON.
  • Extend or modify formatLabelSession for custom label syntax by inserting your formatter into the format.combine chain.

4. Usage Examples

Basic Bot Setup and Message Handling

Demonstrates how to initialize a WPPConnect client, listen for incoming one-to-one messages, and send a simple reply.

const wppconnect = require('../../dist');

wppconnect
  .create()
  .then(client => start(client))
  .catch(err => console.error(err));

function start(client) {
  client.onMessage(message => {
    if (message.body === 'Hi' && !message.isGroupMsg) {
      client
        .sendText(message.from, 'Welcome to WPPConnect')
        .then(response => {
          console.log('Message sent:', response.id);
        })
        .catch(error => {
          console.error('Send error:', error);
        });
    }
  });
}

Practical Usage:

  • Use a WhatsApp Business account; regular accounts aren’t supported.
  • Scan the QR code on first run; subsequent runs reuse the session token.
  • Run:
    node examples/basic/index.js
    
  • Customize the trigger (message.body) and reply text.
  • Persist session data and implement reconnection logic in production.

Bot Functions Example

Implements common commands (ping, send, pin, typing, chat state, buttons) in a simple bot.

Setup & Run

npm install
cd examples/bot-functions
npm start

Core Initialization

const wppconnect = require('../../dist');

wppconnect
  .create({
    session: 'test',
    onLoadingScreen: (percent, message) => {
      console.log('LOADING_SCREEN', percent, message);
    },
  })
  .then(client => start(client))
  .catch(console.error);

function start(client) {
  console.log('Starting bot...');
  client.onMessage(handleMessage);
}

Command Handler

async function handleMessage(msg) {
  const from = msg.from;
  const text = msg.body || '';

  try {
    if (text === '!ping') {
      return client.sendText(from, 'pong');
    }
    if (text === '!ping reply') {
      return client.reply(from, 'pong', msg.id.toString());
    }
    if (text === '!chats') {
      const chats = await client.getAllChats();
      return client.sendText(from, `Open chats: ${chats.length}`);
    }
    if (text === '!info') {
      const info = await client.getHostDevice();
      const message =
        `_*Connection info*_ \n` +
        `• Name: ${info.pushname}\n` +
        `• Number: ${info.wid.user}\n` +
        `• Battery: ${info.battery}%\n` +
        `• Device: ${info.phone.device_manufacturer}\n` +
        `• WA Version: ${info.phone.wa_version}`;
      return client.sendText(from, message);
    }
    if (text.startsWith('!sendto ')) {
      let [_, number, ...msgParts] = text.split(' ');
      const message = msgParts.join(' ');
      if (!number.includes('@c.us')) number += '@c.us';
      return client.sendText(number, message);
    }
    if (text.startsWith('!pin ')) {
      const flag = text.split(' ')[1] === 'true';
      return client.pinChat(from, flag);
    }
    if (text.startsWith('!typing ')) {
      const flag = text.split(' ')[1] === 'true';
      return flag ? client.startTyping(from) : client.stopTyping(from);
    }
    if (text.startsWith('!ChatState ')) {
      const state = text.split(' ')[1]; // 0: Typing, 1: Recording, 2: Paused
      return client.setChatState(from, state);
    }
    if (text === '!btn') {
      return client.sendMessageOptions(from, 'Choose:', {
        title: 'Product Options',
        footer: 'Select below',
        isDynamicReplyButtonsMsg: true,
        dynamicReplyButtons: [
          { buttonId: 'yes', buttonText: { displayText: 'YES' }, type: 1 },
          { buttonId: 'no',  buttonText: { displayText: 'NO'  }, type: 1 },
        ],
      });
    }
  } catch (error) {
    console.error('Command error', error);
  }
}

Practical Tips:

  • Wrap each command in its own if block for clarity.
  • Use sendText for basic replies and reply to reference the original message.
  • Append @c.us to numbers when messaging arbitrary contacts.
  • Explore methods like sendImage and sendFile to extend the bot.
  • Catch errors to prevent crashes on unexpected input.

REST API Example for WPPConnect Client

Exposes WPPConnect operations (connection status, text message, PIX) via an Express REST interface.

1. Setup

npm install express @wppconnect-team/wppconnect
node index.js

Scan the QR code printed in the console to establish the session.

2. Initialization (index.js)

const express = require('express');
const wppconnect = require('@wppconnect-team/wppconnect');
const app = express();
let clientInstance;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

wppconnect
  .create({
    session: 'teste',
    headless: true,
    logQR: true,
    autoClose: 60000,
    folderNameToken: './tokens'
  })
  .then(client => { clientInstance = client; })
  .catch(console.error);

app.listen(3000, () => console.log('Server on port 3000'));

3. Endpoints

GET /getconnectionstatus
Returns

{ "status": true, "message": "CONNECTED" }

POST /sendmessage
Request:

{ "telnumber": "554190000000", "message": "Hello from WPPConnect!" }

Response:

{ "status": true, "message": "<message-id>" }

POST /sendpixmessage
Request:

{
  "telnumber": "554190000000",
  "params": { /* PIX payload */ },
  "options": { /* sendPix options */ }
}

Response format matches /sendmessage.

4. Usage Examples

Check status:

curl http://127.0.0.1:3000/getconnectionstatus

Send text:

curl -X POST http://127.0.0.1:3000/sendmessage \
  -H "Content-Type: application/json" \
  -d '{"telnumber":"554190000000","message":"Hello from REST!"}'

Send PIX:

curl -X POST http://127.0.0.1:3000/sendpixmessage \
  -H "Content-Type: application/json" \
  -d '{
    "telnumber":"554190000000",
    "params": { "pixKey":"...","value":"10.00" },
    "options": { "caption":"Payment QR" }
  }'

Practical Tips:

  • Ensure clientInstance is initialized before handling requests.
  • Append @c.us to telnumber inside the server.
  • Use getConnectionState() to confirm a CONNECTED session.
  • Handle promise rejections to avoid hanging HTTP requests.

Using the Newsletter Example

Initializes a client, listens for a trigger, and creates a newsletter via createNewsletter.

1. Initialize the Client

const wppconnect = require('../../dist');

wppconnect
  .create({ session: 'test' })
  .then(client => start(client))
  .catch(error => console.error('Initialization error', error));

2. Listen for the Trigger Message

function start(client) {
  client.onMessage(async message => {
    if (message.body === 'create newsletter' && !message.isGroupMsg) {
      await handleCreateNewsletter(client, message);
    }
  });
}

3. Create the Newsletter

async function handleCreateNewsletter(client, message) {
  const newsletter = await client.createNewsletter(
    'WPP Test Newsletter2',
    { description: 'test' }
  );

  console.log('Newsletter created:', newsletter);

  await client.sendText(
    message.from,
    '```' + JSON.stringify(newsletter, null, 2) + '```'
  );

  await client.sendText(
    message.from,
    '✅ Newsletter created. Check channels on your device.'
  );
}

4. Practical Usage

node examples/newsletter/index.js
  • Send create newsletter in a private chat to your bot.
  • Receive code-formatted JSON and a confirmation.
  • Extend payloads with images or links.
  • Wrap createNewsletter in try/catch to handle errors.

Order Message Handling Example

Shows how to send an order, retrieve details, and listen for status updates.

Sending an Order Message

if (message.body === 'new order' && !message.isGroupMsg) {
  const items = [
    {
      type: 'custom',
      name: 'Item with cost test',
      price: 120000,
      qnt: 2
    }
  ];

  const order = await client.sendOrderMessage(message.from, items);
  froms.push(message.from);

  await client.sendText(message.from, 'Save your order ID for status checks:');
  await client.sendText(message.from, order.id);
}

Retrieving Order Details

if (message.body?.startsWith('order id=') && !message.isGroupMsg) {
  const orderId = message.body.split('=')[1].trim();
  const orderDetails = await client.getOrder(orderId);

  await client.sendText(
    message.from,
    '```json\n' + JSON.stringify(orderDetails, null, 2) + '\n```'
  );
}

Listening for Order Status Updates

client.onOrderStatusUpdate(update => {
  froms.forEach(async chatId => {
    await client.sendText(
      chatId,
      '```json\n' + JSON.stringify(update, null, 2) + '\n```'
    );
  });
});

Practical Usage Tips:

  • Trigger order flows only in private chats.
  • Persist order.id for status checks.
  • Specify prices in the smallest currency unit (e.g., cents).
  • Maintain a list of chat IDs (froms) for broadcasts.
  • Wrap JSON payloads in code fences for readability.

5. API Reference

This section explains how to access and navigate the automatically generated TypeDoc documentation for WPPConnect. It links to the hosted docs, shows how library layers map to TypeScript namespaces, and offers tips for reading the API types.

5.1 Accessing the Hosted Docs

The public API docs are published to GitHub Pages. View them at:

https://wppconnect-team.github.io/wppconnect/api-docs/

Use the search box or the sidebar to jump to modules, namespaces, interfaces or classes.

5.2 Generating the Docs Locally

Run the following to regenerate TypeDoc output under api-docs/:

npm run docs
# or directly
npx typedoc --options typedoc.json

Key typedoc.json settings:

  • entryPoints: src/index.ts
  • out: api-docs
  • excludePrivate: true
  • includeVersion: true

5.3 Layers → Namespaces Mapping

WPPConnect organizes its public API into logical layers, each exposed as a TypeScript namespace:

• Core
• Namespace: WPPConnect
• Exports: initialize, create, main client class

• Configuration
• Namespace: WPPConnect.Config
• Types: IConfig, ConfigOptions, session settings

• Models
• Namespace: WPPConnect.Models
• Types: Contact, Chat, Message, etc.

• Logging
• Namespace: WPPConnect.Logging
• Functions: setLogLevel, Logger, LogLevel enum

• Tokens
• Namespace: WPPConnect.Tokens
• Types: ITokenStore, TokenData, token management APIs

5.4 Reading the TypeScript Types

  1. Click a namespace in the sidebar (e.g. WPPConnect.Config).
  2. Select an interface or function to view:
    • Parameters
    • Return type
    • Example usage (when provided)

Example: initialize signature

// src/index.ts excerpt
export function initialize(
  options: Config.IConfig
): Promise<WPPConnect>;

In the docs you’ll see:

  • Parameters
    options: Config.IConfig – all configuration flags
  • Returns
    Promise<WPPConnect> – a ready-to-use client instance

5.5 Quick Navigation Tips

  • Use the global search (top right) to find types or methods.
  • Switch between Modules and Namespaces view for hierarchical or flat listing.
  • For method overloads, expand the declaration to see all signatures.
  • Click the “Source” link to jump to the exact TypeScript definition in GitHub.

5.6 Common Reference Patterns

Initialize a client and inspect its methods:

import { initialize, Config } from '@wppconnect-team/wppconnect';

const client = await initialize({
  session: 'user123',
  headless: true,
  logLevel: 'info'
} as Config.IConfig);

// Hover over `client` in your editor to see all methods listed in WPPConnect namespace
await client.sendText('123456789@c.us', 'Hello from WPPConnect!');

Consult the API docs for further details on each namespace, type, and method.

6. Advanced Usage

File to Base64 Conversion (src/api/helpers/file-to-base64.ts)

Purpose
Convert a local file on disk into a Base64-encoded data URI, automatically detecting its MIME type (or accepting an override). Used throughout the sender layer to inline images, audio, documents, etc.

Functions

  • fileToBase64(path: string, mime?: string | false): Promise<string | false>
  • Mine(path: string): Promise<string | false> – Returns only the MIME type via mime-types.lookup

How it works

  1. Check if the file exists on disk (fs.existsSync).
  2. Read the file into a Base64 string (fs.readFileSync(path, { encoding: 'base64' })).
  3. Determine the MIME type:
    • If you pass mime, use it directly.
    • Otherwise try mime-types.lookup(path).
    • If that fails, probe the file contents with file-type.fromFile(path).
    • Fallback to application/octet-stream.
  4. Return a data URI:
    data:<mime>;base64,<base64Payload>
    
  5. If the file doesn’t exist, returns false.

Usage Examples

Importing the helpers

import { fileToBase64, Mine as getMime } from 'src/api/helpers/file-to-base64';

Basic conversion

const dataUri = await fileToBase64('./assets/logo.png');
if (!dataUri) {
  throw new Error('File not found');
}
console.log(dataUri.slice(0, 30));
// → data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...

Forcing a specific MIME type

// Override auto-detection if you know the file really is image/webp
const webpUri = await fileToBase64('path/to/file', 'image/webp');

Getting only the MIME type

const mimeType = await getMime('./docs/tutorial.pdf');
if (mimeType) {
  console.log(mimeType); // e.g. "application/pdf"
}

Practical Tips

  • Always handle the false return to catch missing files.
  • Use the resulting data URI directly in any WPPConnect sender method that accepts Base64 (e.g. sendImageFromBase64, sendFile, sendPttFromBase64).
  • To download a remote URL, call downloadFileToBase64(url) first, then fall back to fileToBase64 for local paths.
  • Passing mime = false behaves like omitting it—auto-detect applies in both cases.

Product Catalog Management

Purpose
Manage products and collections via the CatalogLayer: create, retrieve, update, delete.

Creating a Product

// Add a new product
const product = await client.createProduct(
  'Wireless Mouse',                // name
  base64ImageString,               // image as Base64
  'Ergonomic wireless mouse',      // description
  29.99,                           // price
  false,                           // isHidden
  'https://yourstore.com/mouse',   // url
  'WM-001',                        // retailerId
  'USD'                            // currency
);
console.log('New product ID:', product.id);

Retrieving Products

// Get first 20 products of a business
const products = await client.getProducts('123456789@c.us', 20);
products.forEach(p => {
  console.log(p.name, p.price);
});

Fetching a Single Product

const product = await client.getProductById('123456789@c.us', 'WM-001');
console.log('Description:', product.description);

Editing a Product

await client.editProduct('WM-001', {
  price: '24.99',
  description: 'Now 20% OFF',
  isHidden: false
});

Deleting Products

// Single deletion
await client.delProducts(['WM-001']);

// Batch deletion
await client.delProducts(['WM-002', 'WM-003']);

Managing Product Images

// Change main image
await client.changeProductImage('WM-001', newBase64Image);

// Add additional image
await client.addProductImage('WM-001', extraBase64Image);

// Remove additional image by index
await client.removeProductImage('WM-001', '1');

Collections

// Create a new collection
const collection = await client.createCollection(
  'Summer Essentials',
  ['WM-001','WM-004']
);

// Get collections (limit 5 collections, max 10 products each)
const cols = await client.getCollections('123456789@c.us','5','10');

// Edit existing collection
await client.editCollection(collection.id, {
  collectionName: 'Summer Sale',
  productsToAdd: ['WM-005'],
  productsToRemove: ['WM-004']
});

// Delete a collection
await client.deleteCollection(collection.id);

Visibility & Cart Settings

// Toggle product visibility
await client.setProductVisibility('WM-001', true);

// Enable or disable the in-chat “Add to Cart” button globally:
await client.updateCartEnabled(false);

Managing WhatsApp Communities

Purpose
Use CommunityLayer to create and manage WhatsApp Communities—collections of groups under a single umbrella.

Initialization

import { CommunityLayer } from 'wppconnect-api';  // adjust import to your setup
import { chromium } from 'playwright';            // or puppeteer

const browser = await chromium.launch();
const page = await browser.newPage();
const communityLayer = new CommunityLayer(page, 'session-id');

1. Create a Community

const groupIds = ['12345-67890@g.us', '09876-54321@g.us'];
const community = await communityLayer.createCommunity(
  'Neighborhood Watch',
  'Local community updates and alerts',
  groupIds
);
console.log('Community created:', community.id);

Parameters:

  • name: string – community name
  • description: string – community description
  • groupIds: string[] | Wid[] – IDs of groups to include

2. Deactivate a Community

await communityLayer.deactivateCommunity('11111-22222@g.us');
console.log('Community deactivated');

3. Add or Remove Subgroups

// Add existing groups
await communityLayer.addSubgroupsCommunity(
  '11111-22222@g.us',
  ['33333-44444@g.us']
);

// Remove groups
await communityLayer.removeSubgroupsCommunity(
  '11111-22222@g.us',
  ['33333-44444@g.us']
);

4. Manage Community Participants

// Promote a single participant
await communityLayer.promoteCommunityParticipant(
  '11111-22222@g.us',
  '99999@c.us'
);

// Demote multiple participants
await communityLayer.demoteCommunityParticipant(
  '11111-22222@g.us',
  ['99999@c.us', '88888@c.us']
);

Parameter:

  • participantId: string | string[] | Wid[] – user IDs

5. List Community Participants

const participants = await communityLayer.getCommunityParticipants('11111-22222@g.us');
participants.forEach(p => {
  console.log(p.id, p.isAdmin ? 'Admin' : 'Member');
});

Practical Tips

  • Always pass valid group IDs (<digits>-<digits>@g.us).
  • Participant IDs use the <digits>@c.us format.
  • Use getCommunityParticipants to verify membership changes.
  • Inspect the returned “any” shape in console to adapt your typings.

setProfilePic: Update User Profile Picture

Sets the current user’s profile image from a local file, URL or Base64 string. Validates input, enforces allowed image types, and generates two resized thumbnails (96×96 and 640×640) before uploading via WAPI.

Parameters

  • pathOrBase64: string
    • Local file path (./avatar.png), remote URL (https://…/pic.jpg) or data URI (data:image/jpeg;base64,…).
  • to?: string (optional)
    • Chat ID for a business account or other target. Defaults to your own profile.

Errors

  • code: "empty_file" — no file or Base64 data found.
  • code: "invalid_image" — MIME type is not an image (allowed: png, jpg, jpeg, webp, gif).

Usage example

// 1. From a local file
await client.profile.setProfilePic('./assets/me.png')
  .then(() => console.log('Profile picture updated'))
  .catch(err => console.error(err.code, err.message));

// 2. From a remote URL
await client.profile.setProfilePic('https://example.com/avatar.jpg');

// 3. From an in-memory Base64 string
const base64 = await fs.promises.readFile('./me.webp', 'base64');
await client.profile.setProfilePic(`data:image/webp;base64,${base64}`);

// 4. For another target (e.g., business profile)
await client.profile.setProfilePic('./logo.png', '12345@g.us');

How it works

  1. Detects if input starts with data:. If not, attempts downloadFileToBase64 from URL or fileToBase64 from disk.
  2. Throws empty_file if no Base64 obtained.
  3. Checks MIME via base64MimeType, throws invalid_image if not an image.
  4. Strips the data:image/...;base64, header, decodes to a Buffer.
  5. Uses resizeImg to create two sizes:
    • a: 640×640
    • b: 96×96
  6. Calls WAPI.setProfilePic({ a, b }, to) inside the page context.

Tips

  • Ensure your image is at least 640×640 for best quality.
  • Handle errors by inspecting err.code.
  • Thumbnails are mandatory for WhatsApp Web’s avatar previews.

Decrypting WhatsApp Media Streams

Purpose
Show how to decrypt media downloaded from WhatsApp Web using the magix (synchronous) and newMagix (streaming) helpers in src/api/helpers/decrypt.ts.

Essential Concepts

  • WhatsApp messages include a Base64-encoded mediaKey and encrypted payload.
  • magix returns a fully decrypted Buffer when the entire file is in memory.
  • newMagix returns a Node.js Transform stream for on-the-fly decryption of large files.
  1. magix (synchronous decryption)
    Accepts:
  • fileData: Buffer or ArrayBuffer of encrypted bytes
  • mediaKeyBase64: string from message metadata
  • mediaType: one of Image, Video, Audio, Document, Sticker, PTT
  • expectedSize?: number for padding correction

Returns a decrypted Buffer.

Example:

import axios from 'axios';
import { makeOptions, magix } from './helpers/decrypt';
import * as fs from 'fs';

async function fetchAndDecrypt(
  url: string,
  mediaKey: string,
  mediaType: string,
  expectedSize: number
) {
  // Step 1: Download encrypted data
  const opts = makeOptions('MyApp/1.0');
  const response = await axios.get<ArrayBuffer>(url, opts);
  const encrypted = Buffer.from(response.data);

  // Step 2: Decrypt
  const decrypted: Buffer = magix(encrypted, mediaKey, mediaType, expectedSize);

  // Step 3: Save or process
  await fs.promises.writeFile(`./output.${mediaType.toLowerCase()}`, decrypted);
}
  1. newMagix (streaming decryption)
    Returns a Transform stream you can pipe directly from an HTTP response or file read stream.

Example:

import axios from 'axios';
import * as fs from 'fs';
import { makeOptions, newMagix } from './helpers/decrypt';

async function streamDecryptToFile(
  url: string,
  mediaKey: string,
  mediaType: string,
  expectedSize: number,
  destPath: string
) {
  // Download encrypted stream
  const opts = makeOptions('MyApp/1.0', 'stream');
  const response = await axios.get<NodeJS.ReadableStream>(url, opts);

  // Create decrypt stream
  const decryptStream = newMagix(mediaKey, mediaType, expectedSize);

  // Pipe network → decrypt → file
  response.data.pipe(decryptStream).pipe(fs.createWriteStream(destPath));

  // Await completion
  await new Promise((resolve, reject) => {
    decryptStream.on('end', resolve);
    decryptStream.on('error', reject);
  });
}

Practical Guidance

  • Supply the correct expectedSize when known to ensure proper PKCS#7 padding trimming.
  • Use magix for small media (<10 MB) and newMagix for larger files or direct streaming.
  • Call makeOptions to apply required headers and UA override when fetching from WhatsApp servers.
  • Ensure futoin-hkdf, atob and Node’s crypto are installed and up to date.

7. Internals & Architecture

Deep dive into WPPConnect’s core engine components: how we launch and manage browsers, bootstrap WhatsApp Web, inject APIs, handle sessions, and route events between Node.js and the browser page.


7.1 BrowserController

Manages Puppeteer browser lifecycle, page reuse, and temporary profile cleanup.

initBrowser

Initializes or connects to Chrome/Chromium with WPPConnect defaults.

import { Browser, launch, connect } from 'puppeteer-core';
import { StealthPlugin } from 'puppeteer-extra-plugin-stealth';
import { chromeLauncher } from 'chrome-launcher';

async function initBrowser(
  session: string,
  options: CreateConfig,
  logger: Logger
): Promise<Browser> {
  // 1. Locate Chrome if useChrome=true
  if (options.useChrome) {
    const detected = await chromeLauncher.Launcher.getInstallations();
    options.puppeteerOptions.executablePath = detected[0];
  }

  // 2. Apply stealth
  const puppeteerExtra = require('puppeteer-extra');
  puppeteerExtra.use(StealthPlugin());

  // 3. Connect or launch
  let browser: Browser;
  if (options.browserWS) {
    browser = await connect({ browserWSEndpoint: options.browserWS, ...options.puppeteerOptions });
  } else {
    const args = [...(options.browserArgs || []), ...defaultChromiumArgs()];
    if (options.proxy) args.push(`--proxy-server=${options.proxy.url}`);
    browser = await puppeteerExtra.launch({
      headless: options.headless,
      devtools: options.devtools,
      args,
      ...options.puppeteerOptions
    });
  }

  // 4. Schedule cleanup of temp user-data-dir
  scheduleProfileCleanup(browser, logger);

  return browser;
}

getOrCreatePage

Reuses an existing tab or opens a new one. Ensures only one WhatsApp Web page per session.

const pages = new Map<string, Page>();

export async function getOrCreatePage(browser: Browser, session: string): Promise<Page> {
  if (pages.has(session)) {
    const existing = pages.get(session)!;
    if (!existing.isClosed()) return existing;
  }

  const page = await browser.newPage();
  await page.setUserAgent(customUserAgent());
  pages.set(session, page);
  return page;
}

Practical tips
• Customize default Chromium flags in src/config/puppeteer.config.tschromiumArgs
• Append flags at runtime via options.browserArgs
• Always call getOrCreatePage after initBrowser to avoid duplicate tabs


7.2 SessionManager

Persists and restores authentication state (cookies, localStorage) between runs.

saveSession

Serializes cookies and localStorage to disk:

import fs from 'fs/promises';

export async function saveSession(page: Page, sessionId: string) {
  const cookies = await page.cookies();
  const localStorage = await page.evaluate(() => JSON.stringify(window.localStorage));
  await fs.writeFile(`${sessionId}.session.json`, JSON.stringify({ cookies, localStorage }));
}

restoreSession

Loads from disk and applies to a fresh page:

export async function restoreSession(page: Page, sessionId: string) {
  try {
    const data = JSON.parse(await fs.readFile(`${sessionId}.session.json`, 'utf-8'));
    await page.setCookie(...data.cookies);
    await page.evaluate(ls => {
      Object.entries(JSON.parse(ls)).forEach(([k, v]) => localStorage.setItem(k, v as string));
    }, data.localStorage);
  } catch {
    // No existing session; proceed with QR auth
  }
}

7.3 WAPI Injection & RPC

Injects WPPConnect’s in-page API (WAPI) for messaging, contacts, and media.

Injection Flow

  1. After page load, wait for WhatsApp Web’s webpackJsonp bundle.
  2. Inject wapi.js via page.evaluateOnNewDocument.
  3. Expose a Node callback for events:
await page.exposeFunction('onWAPIEvent', (event) => {
  // route to Node EventEmitter
  eventEmitter.emit(event.type, event.payload);
});
await page.evaluateOnNewDocument(() => {
  // inside browser context
  window.WAPI.on = (type, handler) =>
    window.addEventListener(type, ev => handler(ev.detail));
});

RPC Calls

All WAPI functions use window.postMessage to communicate:

// Node side: send command
await page.evaluate(({ fn, args, id }) => {
  window.postMessage({ type: 'WAPI_CALL', fn, args, id }, '*');
}, { fn: 'sendMessage', args: ['123@c.us', 'Hello'], id: 'req1' });

// Browser side (in wapi.js)
window.addEventListener('message', async ({ data }) => {
  if (data.type === 'WAPI_CALL') {
    const result = await WPP[data.fn](https://github.com/wppconnect-team/wppconnect/blob/master/...data.args);
    window.postMessage({ type: 'WAPI_RESPONSE', id: data.id, result }, '*');
  }
});

7.4 EventDispatcher & Message Queue

Coordinates inbound/outbound events between browser and Node.js.

  • Node enqueues WAPI calls with unique IDs.
  • Browser resolves calls and emits WAPI_RESPONSE.
  • Session-level queue ensures FIFO ordering and backpressure.

Example: sending a text message with guaranteed ordering

class MessageQueue {
  private queue = Promise.resolve();

  enqueue<T>(fn: () => Promise<T>): Promise<T> {
    this.queue = this.queue.then(() => fn());
    return this.queue;
  }
}

// Usage
await messageQueue.enqueue(() => client.sendText('123@c.us', 'Hi'));

7.5 Plugin System & Extensions

Allows users to hook into lifecycle events or override WAPI behavior.

  • Define a plugin as an object { name: string, onBrowser?(browser), onPage?(page), onEvent?(event) }.
  • Register via Client.use(plugin) before client.initialize().
client.use({
  name: 'log-incoming',
  onEvent(event) {
    if (event.type === 'incoming_message') {
      console.log('New message:', event.payload);
    }
  }
});

7.6 Logging & Diagnostics

Unified logging across Node and browser for easy troubleshooting.

  • Node uses Winston (configured in src/config/logger.ts) at info level by default.
  • Browser console logs pipe back via page.on('console', ...).
  • Network requests (media uploads/downloads) log URLs and timings.

Enable verbose logging:

import { createLogger, transports, format } from 'winston';

const logger = createLogger({
  level: 'debug',
  format: format.combine(format.timestamp(), format.simple()),
  transports: [new transports.Console()]
});

const client = createClient({ session: 'X', logger });

Practical tip
• Monitor browser console messages to diagnose injection failures or API mismatches.
• Use client.on('qr', ...) and client.on('ready', ...) hooks to track session bootstrap.

8. Contribution & Development

Overview
This section guides you through cloning the repo, installing dependencies, running tests, linting code, and submitting pull requests to WPPConnect.

Setup

  1. Clone the repository and install dependencies
    git clone https://github.com/wppconnect-team/wppconnect.git
    cd wppconnect
    npm install
    npm run prepare    # installs Git hooks via Husky
    
  2. Configure environment variables (for authenticated tests)
    • TEST_USER_ID – your WhatsApp user ID (e.g., 123456789@c.us)
    • TEST_GROUP_ID – optional group ID for group-scoped tests

Testing

Authenticated Test Wrapper (describeAuthenticatedTest)

Provide end-to-end coverage by booting a logged-in session for Mocha tests.

import { describeAuthenticatedTest, testUserId, testGroupID } from './common';

describeAuthenticatedTest('Message sending', function(getClient) {
  it('sends a text message', async function() {
    const client = getClient();
    const chatId = testUserId!;
    const result = await client.sendText(chatId, 'Hello from tests!');
    if (!result.ack) throw new Error('Message not acknowledged');
  });

  it('creates and sends to a new group', async function() {
    if (!testGroupID) this.skip();
    const client = getClient();
    const { gid } = await client.createGroup('Test Group', [testUserId!]);
    await client.sendText(gid._serialized, 'Hi group!');
  });
});

Key Details

  • Suite timeout: 60 s
  • Session options:
    • session: 'test-authenticated'
    • disableWelcome: true, updatesLog: false, waitForLogin: false
  • Hooks:
    • Skips suite if TEST_USER_ID is unset
    • Suppresses logs (wppconnect.defaultLogger.level = 'none')
    • Calls client.waitForLogin(), skips on failure
    • Closes session with client.close() after tests

Run all tests:

npm test

ESLint Configuration

WPPConnect enforces code quality with ESLint, TypeScript support, custom rules, overrides and npm scripts.

.eslintrc.js (core setup)

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'header', 'prettier'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended'
  ],
  rules: {
    '@typescript-eslint/no-explicit-any': 'off',
    'header/header': ['error', 'block', [
      '',
      ' * This file is part of WPPConnect.',
      ' * …license header lines…'
    ], 1],
    'prettier/prettier': ['error', { endOfLine: 'auto' }]
  },
  overrides: [
    {
      files: ['src/lib/**/*.js'],
      parser: '@babel/eslint-parser',
      plugins: ['@babel'],
      env: { amd: true, commonjs: true, browser: true },
      rules: {
        '@typescript-eslint/no-array-constructor': 'off',
        'no-redeclare': 'off'
      }
    }
  ]
};

Linting Scripts (package.json)

{
  "scripts": {
    "lint:ts": "eslint -c .eslintrc.js --ext .ts src",
    "lint:js": "eslint -c .eslintrc.js --ext .js src",
    "lint": "npm run lint:ts && npm run lint:js"
  }
}

• Run npm run lint to check both TS and JS.
• Auto-fix issues: npx eslint --fix src/<file|dir>.

Practical Usage

  • Husky hooks install via npm run prepare.
  • Commitizen/Commitlint enforce message format.
  • To add or override rules, update rules or overrides in .eslintrc.js.

Commit Message Linting with Commitlint

Enforce Conventional Commits on every PR using Commitlint and GitHub Actions.

.commitlintrc.json

{
  "$schema": "https://json.schemastore.org/commitlintrc.json",
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    "body-max-line-length": [0, "always", 100],
    "header-max-length": [2, "always", 120],
    "subject-case": [1, "never", [
      "sentence-case",
      "start-case",
      "pascal-case",
      "upper-case"
    ]]
  }
}

GitHub Actions Workflow (.github/workflows/commitlint.yml)

name: commit lint
on: [pull_request]

jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Lint commit
        uses: wagoid/commitlint-github-action@v5.5.1
        with:
          configFile: './.commitlintrc.json'

Usage

  • Follow <type>(<scope>): <subject> (e.g., feat(api): add user endpoint).
  • Failures block merging on PRs.

Issue & Pull Request Templates

Enforce structured reporting and submissions using GitHub templates in .github/ISSUE_TEMPLATE/ and .github/PULL_REQUEST_TEMPLATE.md.

Bug Report (.github/ISSUE_TEMPLATE/bug_report.md)

---
name: Bug report
about: Create a report to help us improve
labels: 'bug, needs triage'
---

Sections: Description, Environment, Steps to Reproduce, Log Output, Your Code, Additional context.

Feature Request (.github/ISSUE_TEMPLATE/feature_request.md)

---
name: Feature request
about: "I have a suggestion (and may want to implement it 😊)!"
labels: 'enhancement, needs triage'
---

Fields: Problem, Solution, Alternatives, Additional context.

Support Question (.github/ISSUE_TEMPLATE/support_question.md)

---
name: Support Question
about: 'If you have a question, please check our Docs or Discord!'
labels: 'question, needs triage'
---

Checklist: searched issues/docs, code snippets, expected vs actual behavior.

Pull Request (.github/PULL_REQUEST_TEMPLATE.md)

Fixes #<issue-number>

## Changes proposed in this pull request

- Bullet each change

To test:
```bash
npm install github:<username>/wppconnect#<branch>

Guidelines: reference issues (`Fixes #123`), summarize changes, include test instructions, follow CommonJS conventions.

### Submitting a Pull Request

1. Fork the repo and create a topic branch (`git checkout -b feat/my-feature`).  
2. Implement your feature or fix.  
3. Run tests and lint:  
   ```bash
   npm test
   npm run lint
  1. Commit with Conventional Commits.
  2. Push and open a PR against main.
  3. Ensure GitHub Actions pass all checks before request review.

By following these steps, you’ll maintain code quality, consistent style, and streamlined collaboration.