@ivanamato/whatsapp-inbox
v0.27.0
Published
Embeddable WhatsApp inbox UI library. Framework-agnostic — works with Vue, React, Angular, or plain HTML. Connects directly to the [Evolution API](https://doc.evolution-api.com/) from the browser.
Readme
WhatsApp Inbox
Embeddable WhatsApp inbox UI library. Framework-agnostic — works with Vue, React, Angular, or plain HTML. Connects directly to the Evolution API from the browser.
Built with Preact + Tailwind CSS v4. No backend required.
Features
- Real-time messaging — Auto-polling keeps conversations and messages updated
- Multi-device — Switch between multiple WhatsApp instances via a device selector
- Imperative API — Open conversations, pre-fill the composer, switch devices, send messages, and fetch data from outside the inbox
- Conversation pre-selection & pre-fill — Open a specific conversation programmatically with an optional draft message — ideal for CRM "reply to customer" flows
- Pre-built messages — Per-device library of reusable text templates and audio PTT voice notes, selectable from a searchable picker in the composer
- Custom chat actions — Configurable action buttons per chat row driven by the host app
- Chat tags & filtering — Colored tag pills on each chat, resolved by the host app, with clickable filter chips to narrow the list
- Template messages — Send WhatsApp-approved templates with header, body, and button parameters
- Interactive messages — Button messages with up to 3 custom actions
- Media support — Send and receive images, videos, audio, and documents
- Voice recording — Record and send PTT voice messages directly from the composer
- Image paste — Paste an image from the clipboard to send it with an optional caption
- Message forwarding — Forward messages between conversations
- Context menu — Right-click actions (delete, forward) on messages
- Read-only mode — View-only access per device
- CSS isolation — All styles use the
wa:prefix to avoid collisions with host apps
Installation
Published to npm as @ivanamato/whatsapp-inbox.
npm install @ivanamato/whatsapp-inboxQuick Start
Using the UMD bundle (any framework or plain HTML)
<link rel="stylesheet" href="whatsapp-inbox.css" />
<script src="whatsapp-inbox.umd.js"></script>
<div id="inbox" style="height: 100vh;"></div>
<script>
const inbox = WhatsAppInbox.mount(document.getElementById('inbox'), {
devices: [
{
id: 'main',
label: 'My WhatsApp',
apiUrl: 'https://your-evolution-api.com',
instanceToken: 'your-instance-token',
instanceName: 'your-instance',
},
],
});
// Later: inbox.unmount();
</script>Using the ES module
import { mount } from '@ivanamato/whatsapp-inbox';
import '@ivanamato/whatsapp-inbox/style.css';
const inbox = mount(document.getElementById('inbox'), {
devices: [
{
id: 'main',
label: 'My WhatsApp',
apiUrl: 'https://your-evolution-api.com',
instanceToken: 'your-instance-token',
instanceName: 'your-instance',
},
],
});Documentation
mount(element, config)
Mounts the inbox into a DOM element and returns a WhatsAppInbox handle.
function mount(element: HTMLElement, config: WhatsAppMultiDeviceConfig): WhatsAppInbox
function unmount(element: HTMLElement): voidconfig.devices — Device configuration
The devices array is the only required field. Each entry represents one WhatsApp instance.
type DeviceConfig = {
id: string;
label?: string;
apiUrl: string;
instanceToken: string;
instanceName: string;
providerType?: 'evolution' | 'cloud';
readonly?: boolean;
prebuiltMessages?: PrebuiltMessage[];
};| Field | Required | Description |
|---|---|---|
| id | Yes | Unique identifier for this device — used to target it via the imperative API |
| apiUrl | Yes | Evolution API base URL (e.g. https://your-api.com) |
| instanceToken | Yes | Per-instance token from Evolution API — see Security |
| instanceName | Yes | Evolution API instance name |
| label | No | Display name shown in the device selector dropdown |
| providerType | No | 'evolution' (default) or 'cloud' |
| readonly | No | If true, hides the composer — agents can read but not send |
| prebuiltMessages | No | Reusable message templates and audio voice notes — see Pre-built Messages |
Minimal example:
mount(el, {
devices: [
{
id: 'support',
apiUrl: 'https://your-evolution-api.com',
instanceToken: 'your-per-instance-token',
instanceName: 'support-instance',
label: 'Support',
},
],
});Multi-device example:
mount(el, {
devices: [
{
id: 'sales',
label: 'Sales',
apiUrl: 'https://your-evolution-api.com',
instanceToken: 'token-for-sales-instance',
instanceName: 'sales',
},
{
id: 'support',
label: 'Support (read-only)',
apiUrl: 'https://your-evolution-api.com',
instanceToken: 'token-for-support-instance',
instanceName: 'support',
readonly: true,
},
],
defaultDeviceId: 'sales',
});config.defaultDeviceId
ID of the device to select on mount. Defaults to the first device in the array.
mount(el, {
devices: [/* ... */],
defaultDeviceId: 'support',
});config.translations
Override any UI string. All keys are optional — only provide what you want to change.
mount(el, {
devices: [/* ... */],
translations: {
'messageView.typeMessage': 'Write a message…',
'messageView.sendTemplate': 'Use template',
'conversationList.searchPlaceholder': 'Search conversations',
},
});config.chatActions — Action buttons per chat row
Adds a three-dot menu to every chat row. Clicking it opens a dialog showing the contact's profile picture, name, phone number, device name, and your custom action buttons.
type ChatActionsResolver = (
chat: Chat,
device: DeviceConfig,
) => ChatAction[] | Promise<ChatAction[]>;
type ChatAction = {
id: string;
label: string;
icon?: ComponentType<{ className?: string }>; // e.g. from lucide-react
onClick: (chat: Chat, device: DeviceConfig) => void;
};Synchronous example:
mount(el, {
devices: [/* ... */],
chatActions: (chat, device) => [
{
id: 'open-crm',
label: 'Open in CRM',
onClick: () => window.open(`https://crm.example.com/contacts/${chat.phoneNumber}`),
},
{
id: 'tag-vip',
label: 'Tag as VIP',
onClick: () => fetch('/api/tag', {
method: 'POST',
body: JSON.stringify({ phone: chat.phoneNumber, tag: 'vip' }),
}),
},
],
});Async example — different actions based on CRM status:
mount(el, {
devices: [/* ... */],
chatActions: async (chat, device) => {
const customer = await fetch(`/api/crm/lookup?phone=${chat.phoneNumber}`)
.then(r => r.json())
.catch(() => null);
if (customer) {
return [
{
id: 'view-customer',
label: 'See Customer',
onClick: () => window.open(`/crm/customers/${customer.id}`),
},
{
id: 'create-ticket',
label: 'Create Support Ticket',
onClick: () => createTicket(customer.id, chat),
},
];
}
return [
{
id: 'create-contact',
label: 'Create Contact in CRM',
onClick: () => createContact(chat.phoneNumber, device.id),
},
];
},
});Behavior:
- The three-dot icon always renders on every chat row, regardless of whether
chatActionsis configured - Custom buttons only appear when a resolver is provided
- A loading spinner shows while the resolver is running
- After clicking an action, the dialog closes automatically
- Dismiss with X, backdrop click, or Escape
config.chatTags — Colored tag pills on chat rows
Displays colored pills below the contact name in each row. All unique tags across all chats appear as clickable filter chips above the list.
type ChatTagsResolver = (
chat: Chat,
device: DeviceConfig,
) => ChatTag[] | Promise<ChatTag[]>;
type ChatTag = {
id: string; // Unique identifier — also used as the filter key
label: string; // Text shown in the pill
color?: string; // Text color (default: 'white')
background?: string; // Background color (default: '#00a884')
};Example:
mount(el, {
devices: [/* ... */],
chatTags: async (chat, device) => {
const tags = [];
if (chat.unreadCount && chat.unreadCount > 0) {
tags.push({ id: 'unread', label: 'Unread', background: '#ff9800' });
}
if (chat.contactName) {
tags.push({ id: 'saved', label: 'Saved Contact', background: '#2196f3' });
}
// Fetch custom tags from your backend
const remote = await fetch(`/api/tags?phone=${chat.phoneNumber}`)
.then(r => r.json())
.catch(() => []);
return [...tags, ...remote];
},
});Filtering behavior:
- Clicking a tag chip above the list toggles it as a filter
- When multiple chips are selected, only chats matching all selected tags are shown (AND logic)
- Tags re-resolve automatically on every poll cycle
config.devices[].prebuiltMessages — Pre-built Messages
A per-device library of reusable message templates. When configured, a book icon button appears in the composer. Clicking it opens a searchable picker with live previews of every item.
type PrebuiltMessage = {
id: string;
label: string; // Short title shown in the picker
content: string; // Text body, or base64-encoded media data
type?: 'text' | 'audio' | 'image' | 'video'; // Default: 'text'
mimeType?: string; // Required for media types. Defaults per type (see table below)
};Types and picker behaviour
| type | Picker preview | Send action | Default mimeType |
|---|---|---|---|
| 'text' | Content text (truncated) | Fills the composer — agent can edit before sending | — |
| 'audio' | Inline <audio> player — fully playable before sending | Explicit Send button (avoids accidental sends while using audio controls) | audio/ogg |
| 'image' | Thumbnail <img> decoded from base64 | Click anywhere on the row | image/jpeg |
| 'video' | Silent <video> preview frame decoded from base64 | Click anywhere on the row | video/mp4 |
All media types (audio, image, video) send immediately without touching the text composer.
Text templates
mount(el, {
devices: [
{
id: 'support',
// ...credentials...
prebuiltMessages: [
{ id: 'greeting', label: 'Greeting', content: 'Hello! How can I help you today?' },
{ id: 'followup', label: 'Follow up', content: "I'm following up on our previous conversation. Please let me know if you have any questions." },
{ id: 'closing', label: 'Closing', content: 'Thank you for your time! Have a great day.' },
{ id: 'wait', label: 'Please wait', content: 'Just a moment while I look into that for you.' },
{ id: 'escalate', label: 'Escalation notice', content: "I'm going to transfer you to a specialist who can better assist you." },
],
},
],
});Audio voice notes
Pre-record voice messages and encode them as base64. The picker renders a fully playable <audio> player so agents can review the recording before hitting Send.
import { readFileSync } from 'fs';
const voiceGreeting = readFileSync('voice-greeting.ogg').toString('base64');
const voiceClosing = readFileSync('voice-closing.ogg').toString('base64');
mount(el, {
devices: [
{
id: 'support',
// ...credentials...
prebuiltMessages: [
{
id: 'voice-greeting',
label: 'Voice Greeting',
type: 'audio',
mimeType: 'audio/ogg', // Recommended for WhatsApp
content: voiceGreeting,
},
{
id: 'voice-closing',
label: 'Voice Closing',
type: 'audio',
mimeType: 'audio/ogg',
content: voiceClosing,
},
],
},
],
});Accepted audio formats: audio/ogg (recommended for WhatsApp), audio/webm, audio/mp4, audio/mpeg.
Images
The picker renders the actual image as a thumbnail so agents see exactly what they are about to send.
import { readFileSync } from 'fs';
const promoBanner = readFileSync('promo-banner.jpg').toString('base64');
const productShot = readFileSync('product-shot.png').toString('base64');
mount(el, {
devices: [
{
id: 'sales',
// ...credentials...
prebuiltMessages: [
{
id: 'promo-banner',
label: 'Promo Banner',
type: 'image',
mimeType: 'image/jpeg',
content: promoBanner,
},
{
id: 'product-shot',
label: 'Product Shot',
type: 'image',
mimeType: 'image/png',
content: productShot,
},
],
},
],
});Accepted image formats: image/jpeg, image/png, image/webp, image/gif.
Videos
The picker renders a silent preview frame so agents can verify the clip before sending.
import { readFileSync } from 'fs';
const demoVideo = readFileSync('product-demo.mp4').toString('base64');
mount(el, {
devices: [
{
id: 'sales',
// ...credentials...
prebuiltMessages: [
{
id: 'product-demo',
label: 'Product Demo',
type: 'video',
mimeType: 'video/mp4',
content: demoVideo,
},
],
},
],
});Accepted video formats: video/mp4 (recommended), video/webm.
Mixed library (recommended for most teams)
prebuiltMessages: [
// Text — agent reviews and can edit before sending
{ id: 'greeting', label: 'Greeting', content: 'Hello! How can I help you today?' },
{ id: 'followup', label: 'Follow up', content: "I'm following up on our previous conversation." },
{ id: 'closing', label: 'Closing', content: 'Thank you for your time! Have a great day.' },
// Audio — agent listens via the inline player, then clicks Send
{ id: 'voice-hi', label: 'Voice: Hi!', type: 'audio', mimeType: 'audio/ogg', content: '<base64>' },
{ id: 'voice-bye', label: 'Voice: Bye!', type: 'audio', mimeType: 'audio/ogg', content: '<base64>' },
// Image — thumbnail preview, click row to send
{ id: 'promo', label: 'Promo Banner', type: 'image', mimeType: 'image/jpeg', content: '<base64>' },
// Video — silent frame preview, click row to send
{ id: 'demo', label: 'Product Demo', type: 'video', mimeType: 'video/mp4', content: '<base64>' },
],Picker behaviour
- The book icon button only appears when
prebuiltMessagesis configured and non-empty for the active device - Search — filters on
labelfor all types; also matchescontentfor text items. Base64 media content is never searched - Audio items have a dedicated Send button to prevent audio control interactions (play/seek/pause) from accidentally triggering a send
- Image and video items — click anywhere on the row to send
- Dismiss with Escape, the X button, or clicking the backdrop
- Each device can have a completely different message library
Imperative API — WhatsAppInbox
mount() returns a WhatsAppInbox object for controlling the inbox programmatically.
type WhatsAppInbox = {
getChats(): Promise<Chat[]>;
getMessages(chatId: string, limit?: number): Promise<Message[]>;
sendText(params: SendTextParams): Promise<SendResult>;
getConnectionState(): Promise<'open' | 'close' | 'connecting'>;
getActiveDevice(): string | null;
setActiveDevice(deviceId: string): void;
selectConversation(phoneNumber: string, prefillMessage?: string, deviceId?: string): void;
openChat(phoneNumber: string, options?: { prefillMessage?: string; deviceId?: string }): void;
unmount(): void;
};getChats()
Fetches all chats for the currently active device.
const chats = await inbox.getChats();
console.log(`${chats.length} conversations on ${inbox.getActiveDevice()}`);
// Find a specific contact
const chat = chats.find(c => c.phoneNumber === '5511999999999');
console.log(chat?.contactName, chat?.unreadCount);getMessages(chatId, limit?)
Fetches messages for a specific chat. limit defaults to 50.
const chats = await inbox.getChats();
const messages = await inbox.getMessages(chats[0].id, 100);
const lastInbound = messages.filter(m => m.direction === 'inbound').at(-1);
console.log('Last customer message:', lastInbound?.content);sendText(params)
Sends a text message from the active device.
const result = await inbox.sendText({
to: '[email protected]',
body: 'Hello from the imperative API!',
});
console.log('Message ID:', result.messageId);getConnectionState()
Checks whether the active device's WhatsApp connection is open.
const state = await inbox.getConnectionState();
// 'open' | 'close' | 'connecting'
if (state !== 'open') {
alert('WhatsApp is disconnected — please scan the QR code.');
}getActiveDevice() / setActiveDevice(deviceId)
Read or change the currently selected device.
console.log('Active device:', inbox.getActiveDevice()); // 'sales'
inbox.setActiveDevice('support');
console.log('Active device:', inbox.getActiveDevice()); // 'support'selectConversation(phoneNumber, prefillMessage?, deviceId?)
Opens a conversation by phone number, optionally pre-filling the composer and/or switching to a specific device. This is the primary integration point for CRM and support tools.
// Open a conversation on the active device
inbox.selectConversation('5511999999999');
// Pre-fill the composer — the agent reviews and clicks Send
inbox.selectConversation('5511999999999', 'Hello, following up on your request!');
// Switch to a specific device, then open the conversation
inbox.selectConversation('5511999999999', undefined, 'support');
// Switch device + pre-fill — the most common CRM integration pattern
inbox.selectConversation('5511999999999', 'Hi! How can I help?', 'support');Pre-fill behavior:
- The message is written into the composer exactly as provided
- The agent can edit it before sending — it is never sent automatically
- Calling
selectConversationagain replaces the previous pre-fill (each call is tracked by an internal counter, so stale values are never applied)
Device switching behavior:
| Mode | deviceId provided | Result |
|---|---|---|
| Single (viewMode: 'single') | Different from active | Switches device → fetches chat list → selects conversation |
| Single | Same as active or omitted | Searches current chat list immediately |
| Merged (viewMode: 'all') | Any | No device switch — deviceId only disambiguates duplicate phone numbers |
If the phone number is not found in the chat list, nothing is selected.
CRM integration example:
// When an agent clicks "Reply" in your CRM
document.getElementById('reply-btn').addEventListener('click', () => {
inbox.selectConversation(
customer.phoneNumber,
`Hi ${customer.firstName}, thanks for reaching out! How can I help?`,
customer.assignedWhatsAppDevice,
);
});Notification / webhook integration example:
// Open the conversation when a new inbound message webhook fires
websocket.on('message', (event) => {
if (event.type === 'inbound') {
inbox.selectConversation(event.phoneNumber, undefined, event.deviceId);
}
});openChat(phoneNumber, options?)
Opens a conversation by phone number. Unlike selectConversation, this works even if the number has no existing chat history — it opens a blank message view so the agent can send the first message.
Pass a plain phone number in international format (digits only — no +, spaces, or @). The Evolution API resolves the WhatsApp JID automatically.
// Open a blank thread for any phone number
inbox.openChat('5511999999999');
// Pre-fill the composer
inbox.openChat('5511999999999', { prefillMessage: 'Hello, how can I help?' });
// Target a specific device
inbox.openChat('5511999999999', { deviceId: 'support' });
// Full form
inbox.openChat('5511999999999', {
prefillMessage: 'Hi! Following up on your request.',
deviceId: 'support',
});Compared to selectConversation:
| | selectConversation | openChat |
|---|---|---|
| Number in chat list | ✅ Opens it | ✅ Opens it |
| Number NOT in list | ❌ Does nothing | ✅ Opens blank thread |
Device behaviour: Same as selectConversation — if deviceId differs from the active device, the inbox switches to that device first. In merged mode, deviceId disambiguates duplicate numbers.
unmount()
Removes the inbox from the DOM and cleans up all subscriptions and timers.
// When navigating away
inbox.unmount();TypeScript types
type Chat = {
id: string;
phoneNumber: string;
contactName?: string;
profilePicUrl?: string;
lastActiveAt?: string;
lastMessage?: {
content: string;
direction: 'inbound' | 'outbound';
type?: string;
};
unreadCount?: number;
};
type Message = {
id: string;
direction: 'inbound' | 'outbound';
content: string;
createdAt: string;
status?: string;
phoneNumber: string;
hasMedia: boolean;
mediaData?: {
url: string;
contentType?: string;
filename?: string;
byteSize?: number;
};
messageType: string;
caption?: string | null;
filename?: string | null;
mimeType?: string | null;
metadata?: Record<string, unknown>;
};
type SendTextParams = { to: string; body: string };
type SendResult = { messageId: string; status?: string };Security
This library uses per-instance tokens instead of the Evolution API global key. Each token is scoped to a single WhatsApp instance — if a token is compromised, only that instance is affected.
To obtain the per-instance token:
curl -H "apikey: YOUR_GLOBAL_KEY" https://your-api.com/instance/fetchInstancesEach instance in the response has a token field — use that as instanceToken.
Never use the global API key in client-side code. The global key grants access to all instances and should only be used server-side for administration tasks.
Examples
See the examples/ directory:
examples/vue3/— Vue 3 Composition API integration with chat tags, async chat actions, imperative API demos, and full Brazilian/international fixture data
To run the Vue 3 example against the mock server:
make docker # start frontend (5173) + mock server (3002)
make example-vue3 # build library + serve example at http://localhost:5174/examples/vue3/Development
Prerequisites
- Node.js 20+
- An Evolution API instance with CORS enabled (
CORS_ORIGIN=*), or use the built-in mock server (no real API needed)
Setup
make installRunning with the mock server (no real API needed)
The fastest way to get started. Starts a local Evolution API mock on port 3002 and the Vite dev server on port 5173 — no credentials required.
With Docker (recommended):
make docker # Build images and start in detached mode
# Open http://localhost:5173
make docker-restart # Rebuild and restart (after code changes)
make docker-down # Stop the stackWithout Docker:
make mock # Starts mock server + Vite dev server concurrently
# Open http://localhost:5173Both modes use devices.json pre-configured to point at http://localhost:3002 with two mock instances (MOCK1 and MOCK2).
Running with a real Evolution API
Replace devices.json with your real credentials:
{
"devices": [
{
"id": "dev-1",
"label": "Dev Instance",
"apiUrl": "https://your-evolution-api.com",
"instanceToken": "your-per-instance-token",
"instanceName": "your-instance"
}
]
}Then run:
make devCommands
make install # Install dependencies
make dev # Start Vite dev server (reads devices.json)
make build # Build library → dist/
make preview # Preview the built library
make lint # Run ESLint
make typecheck # Run TypeScript type check
make clean # Remove dist/ and node_modules/
make test # Run unit tests (Vitest)
make test-e2e # Run e2e tests — requires: make docker
make example-vue3 # Build library + serve Vue 3 example at localhost:5174 — requires: make docker
make mock # Start mock server + dev server locally (no Docker)
make docker # Start full stack with Docker Compose
make docker-build # Rebuild Docker images from scratch
make docker-down # Stop Docker Compose stack
make docker-restart # Stop, rebuild, and restart Docker Compose stack
just release-minor # Bump minor, push tag, create release → publishes to npm
just release-major # Bump major, push tag, create release → publishes to npmBuild Output
dist/
whatsapp-inbox.es.js ~480 KB (ES module)
whatsapp-inbox.umd.js ~305 KB (UMD, works with <script> tags)
whatsapp-inbox.css ~37 KB (prefixed styles)
index.d.ts (TypeScript declarations)All dependencies (including Preact) are bundled — no peer dependencies required.
Architecture
src/
index.ts # Library entry: mount() / unmount() API
App.tsx # Main layout (teal bar, sidebar, message view)
dev.tsx # Dev server entry (reads devices.json)
components/
instance-selector.tsx # Device selector dialog
conversation-list.tsx # Left sidebar with chat list
message-view.tsx # Right panel with messages and composer
prebuilt-messages-dialog.tsx # Pre-built message picker
image-paste-modal.tsx # Clipboard image paste overlay
media-message.tsx # Image/video/document/audio rendering
audio-player.tsx # Custom audio player with waveform
forward-message-dialog.tsx
template-selector-dialog.tsx
interactive-message-dialog.tsx
message-context-menu.tsx
ui/ # shadcn/ui components (dialog, scroll-area, etc.)
lib/
providers/
types.ts # WhatsAppProvider interface, DeviceConfig, Chat, Message
evolution.ts # Evolution API v2 implementation (browser fetch)
index.ts # createProvider() factory
provider-context.tsx # Preact context for multi-device state
hooks/
use-auto-polling.ts # Polling hook with tab-visibility detection
use-cases/
use-app-state.ts # Top-level app state (selected conversation, device)
use-chat-list.ts # Chat list fetching and polling
use-message-thread.ts # Message thread, sending, optimistic updates
use-device-status.ts # Connection status polling
mock-server/
index.ts # Entry point (serves on MOCK_PORT, default 3002)
app.ts # Hono router — all Evolution API routes
fixtures.ts # Per-instance fixture data (MOCK1, MOCK2)
store.ts # In-memory state (sent messages, media, contacts, unread)Key Patterns
- No backend — All API calls (
findChats,sendText,getMediaUrl, etc.) happen directly from the browser via the provider abstraction. - Per-instance tokens — Each device authenticates with its own scoped token, not the global API key.
- Provider system — Implement the
WhatsAppProviderinterface to add new backends. Currently supports Evolution API v2. - Preact with compat — Uses Preact with
preact/compatso React-ecosystem libraries work unchanged. - CSS isolation — Tailwind v4 with
wa:prefix (e.g.wa:flex,wa:p-4). CSS variables namespaced as--wa-*. - Optimistic sends — Messages appear immediately in the thread with a pending status; the chat list refreshes after the send resolves.
- Chat list reactivity —
ConversationListexposes arefresh()ref method called after sending, template sends, and on mount.
Mock Server
The mock server (mock-server/) is a Hono HTTP server that replicates the Evolution API v2 contract for local development — no real WhatsApp credentials needed.
Implemented endpoints:
| Endpoint | Method | Description |
|---|---|---|
| /instance/connectionState/:instance | GET | Returns { instance: { state: 'open' } } |
| /chat/findChats/:instance | POST | Returns chat list, dynamically updated with sent messages and unread counts |
| /chat/findContacts/:instance | POST | Returns contacts, merged with dynamically created contacts |
| /chat/findMessages/:instance | POST | Returns paginated messages; marks chat as read |
| /chat/getBase64FromMediaMessage/:instance | POST | Returns stored base64 for sent media |
| /message/sendText/:instance | POST | Stores message, triggers delivery progression and auto-reply |
| /message/sendMedia/:instance | POST | Stores message + base64 payload |
| /message/sendButtons/:instance | POST | Stores button message |
| /chat/deleteMessageForEveryone/:instance | DELETE | Marks message deleted; emits REVOKE event |
Mock instances:
MOCK1— Brazilian contacts (Ana Beatriz, Carlos Eduardo, Equipe Vendas group, Fernanda Lima, Roberto Mendes)MOCK2— International contacts (Sarah Johnson, James Wright, Product Team group, Miguel Torres)
Publishing
Package is published to npm as @ivanamato/whatsapp-inbox. A GitHub Actions workflow (.github/workflows/publish.yml) automatically builds and publishes on every GitHub Release using Trusted Publishing (OIDC).
- Release commands:
just release-minororjust release-majorbump the version, push the tag, and create a GitHub release (which triggers the publish workflow). publishConfiginpackage.jsonsets"access": "public".
License
MIT
