# Cloudflare Session Proxy (/docs/client-and-integrations/cf-proxy) # Cloudflare Session Proxy [#cloudflare-session-proxy] The Cloudflare Session Proxy lets a local Easy API session make an outbound WebSocket connection to a Cloudflare Worker. Remote consumers call the Worker, so the local machine needs no inbound port. The Worker is still a remotely reachable authenticated entry point, and it must be protected with its consumer token and any Easy API key. ## Topology and limits [#topology-and-limits] ```text Remote consumer --HTTPS/WebSocket--> Cloudflare Worker --Durable Object--> local TunnelClient --HTTP--> Easy API ``` The Worker authenticates `/sessions/:sessionId/upstream` with `UPSTREAM_TOKEN` and all other session paths with `CONSUMER_TOKEN`. It routes each session ID to one Durable Object. HTTP proxy requests wait at most 30 seconds; an absent upstream returns `503 Session offline`, and a timeout returns `504 Gateway Timeout from session upstream`. The Durable Object uses Cloudflare's WebSocket hibernation APIs and an edge ping/pong response, which can reduce active compute while the connection is idle. It does not make the total deployment free: Worker requests, Durable Objects, storage, egress, domains, and account-plan charges remain subject to current Cloudflare billing. Check [Cloudflare Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/) for the account and plan you use. ## Deploy from an empty directory [#deploy-from-an-empty-directory] `@open-wa/cf-proxy` is currently a private monorepo package, so it is not installed from npm. A fresh deployment starts with the repository checkout that contains the Worker entrypoint. ```bash git clone https://github.com/open-wa/wa-automate-nodejs.git cd wa-automate-nodejs corepack enable pnpm install cd packages/cf-proxy pnpm exec wrangler login pnpm exec wrangler whoami ``` `pnpm exec wrangler whoami` must show the Cloudflare account that owns the Worker and its Durable Object namespace. This checkout already contains `src/index.ts`, `src/tunnel-do.ts`, and `wrangler.jsonc`; do not `cd` into a path that does not exist in a standalone npm consumer. The checked-in `wrangler.jsonc` is the binding and migration required by the entrypoint: ```jsonc { "name": "open-wa-proxy", "main": "src/index.ts", "compatibility_date": "2024-03-20", "durable_objects": { "bindings": [{ "name": "SESSION_TUNNEL", "class_name": "SessionTunnel" }] }, "migrations": [{ "tag": "v1", "new_classes": ["SessionTunnel"] }] } ``` Set both Worker secrets without putting their values in the repository or shell history: ```bash export OPENWA_PROXY_UPSTREAM_TOKEN="$(openssl rand -hex 32)" export OPENWA_PROXY_CONSUMER_TOKEN="$(openssl rand -hex 32)" printf '%s' "$OPENWA_PROXY_UPSTREAM_TOKEN" | pnpm exec wrangler secret put UPSTREAM_TOKEN printf '%s' "$OPENWA_PROXY_CONSUMER_TOKEN" | pnpm exec wrangler secret put CONSUMER_TOKEN pnpm exec wrangler deploy ``` Record the deployed `workers.dev` hostname as `OPENWA_PROXY_HOST`, for example `https://open-wa-proxy..workers.dev`. Secrets are independent: the local session needs the upstream token, while remote callers need the consumer token. ## Deploy from an existing checkout [#deploy-from-an-existing-checkout] Contributors who already have the repository use the same entrypoint and binding from the repository root: ```bash pnpm install pnpm --filter @open-wa/cf-proxy exec wrangler whoami pnpm --filter @open-wa/cf-proxy exec wrangler deploy ``` Run `secret put` again only when rotating a value. The Worker code reads the secrets by the exact names `UPSTREAM_TOKEN` and `CONSUMER_TOKEN`; renaming them requires a runtime change. ## Attach a local session [#attach-a-local-session] Run the CLI on the machine that owns the WhatsApp browser session. It keeps the upstream WebSocket open and reconnects after a disconnect: ```bash export OPENWA_PROXY_HOST='https://open-wa-proxy..workers.dev' npx @open-wa/wa-automate@5.1.0 \ --session-id sales \ --host 127.0.0.1 \ --port 8080 \ --proxy-host "$OPENWA_PROXY_HOST" \ --proxy-token "$OPENWA_PROXY_UPSTREAM_TOKEN" ``` The CLI converts the HTTPS host to a WebSocket host and connects to `/sessions/sales/upstream`. The Worker does not run WhatsApp or the browser; those stay on the local machine and still determine session readiness and Easy API behavior. ## Consume the session [#consume-the-session] Use the consumer token in `Authorization: Bearer` rather than a URL query whenever possible, because query strings are more likely to appear in access logs: ```bash curl -i \ "${OPENWA_PROXY_HOST}/sessions/sales/api/getHostNumber" \ -H "Authorization: Bearer $OPENWA_PROXY_CONSUMER_TOKEN" ``` For an Easy API command with a body, include the local API key too when the session was started with `--api-key`: ```bash curl -i -X POST \ "${OPENWA_PROXY_HOST}/sessions/sales/api/sendText" \ -H "Authorization: Bearer $OPENWA_PROXY_CONSUMER_TOKEN" \ -H "X-API-Key: $WA_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"args":["1234567890@c.us","Hello through Cloudflare"]}' ``` The Worker forwards the request to the local Easy API through the upstream connection. A remote caller with the consumer token but no required Easy API key still receives the local API's authentication response. Node clients can use the compatibility URL, which selects the tunnel transport in `@open-wa/socket-client`: ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect( `cf-proxy://open-wa-proxy..workers.dev?sessionId=sales&token=${encodeURIComponent(process.env.OPENWA_PROXY_CONSUMER_TOKEN)}`, ); console.log(await client.getHostNumber()); ``` ## Custom domain [#custom-domain] Keep `workers.dev` until the Worker works end to end. When a Cloudflare zone is ready, add a Worker custom domain or route in the Cloudflare dashboard or Wrangler configuration, deploy again, and replace `OPENWA_PROXY_HOST` with the resulting `https://` hostname. The local session and remote consumers must use the same Worker hostname; a DNS record pointing at another Worker will produce a valid-looking but incorrect endpoint. ## Troubleshooting [#troubleshooting] | Symptom | Command or observation | Expected result | Next action | | ------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Worker deploy fails before creating a route | `pnpm exec wrangler whoami` and inspect `wrangler.jsonc` | The account is authenticated and `SESSION_TUNNEL` binds `SessionTunnel`. | Log in to the intended account and deploy from `packages/cf-proxy`, or fix the binding/migration file. | | Local CLI reports unauthorized upstream | Check the CLI log and `pnpm exec wrangler secret list` | The CLI log should say it connected upstream; the secret list contains `UPSTREAM_TOKEN`. | Set that secret to the exact value passed to `--proxy-token`, then restart the CLI. | | Consumer gets `401 Unauthorized Consumer` | Repeat `curl` with the `Authorization` header above | A matching consumer token reaches the Worker. | Rotate `CONSUMER_TOKEN` if necessary and reconnect clients with the new value. | | Consumer gets `503 Session offline` | Check the local CLI log for `Connected upstream for session sales` | The upstream WebSocket is open for the same session ID. | Start the local CLI, match `--session-id` to the URL path, and resolve local session readiness. | | Consumer gets `504` | Call the same `/api/...` path against `http://127.0.0.1:8080` | The local request completes within 30 seconds. | Fix the local API, browser, or Easy API key before investigating Cloudflare routing. | # Chatwoot integration (/docs/client-and-integrations/chatwoot) # Chatwoot integration [#chatwoot-integration] Use this integration when you want support agents to handle WhatsApp conversations from a Chatwoot inbox. Incoming WhatsApp messages create or reopen Chatwoot conversations. Chatwoot replies return through the connected open-wa session. The current plugin package is `@open-wa/integration-chatwoot@5.1.0`. Chatwoot's account and API behavior can vary by self-hosted release, so record the Chatwoot version alongside your deployment runbook and verify the token permissions against that release. ## Chatwoot-side setup [#chatwoot-side-setup] Start in Chatwoot before you start the open-wa runtime. The integration can create the API inbox for you, but you still need to know the Chatwoot pieces it expects. 1. Open the Chatwoot account you want WhatsApp conversations to appear in. 2. Create a user access token from your Chatwoot profile settings. The token must be able to create contacts, conversations, messages, and inboxes for the target account. 3. Decide whether open-wa or an administrator will create the inbox. 4. If you create the inbox, create an API inbox. Use the public open-wa webhook URL as the inbox webhook URL. 5. Keep the account ID and inbox ID handy. You can copy them from the Chatwoot URL after opening the account or inbox. The easiest path is to pass an account URL such as `https://app.chatwoot.com/api/v1/accounts/123` and let the plugin create or find the matching API inbox. If you already have an inbox, pass an inbox URL such as `https://app.chatwoot.com/api/v1/accounts/123/inboxes/456`. ## Inbox/webhook config [#inboxwebhook-config] Use an **API** inbox in Chatwoot. The webhook URL must point back to the open-wa plugin route: ```text https://wa.example.com/plugins/chatwoot/webhook?api_key=your-secure-key ``` The matching Chatwoot inbox values are: | Field | Value | | ------------ | ------------------------------------------------------------------------- | | Channel type | API | | Inbox name | `open-wa-` or any clear name | | Phone number | the WhatsApp host number connected to open-wa | | Webhook URL | `https://wa.example.com/plugins/chatwoot/webhook?api_key=your-secure-key` | If `forceUpdateCwWebhook` is `true`, open-wa patches the inbox webhook URL during startup. Use that when the public API host changes or when you want config to be the source of truth. ## Exact open-wa config [#exact-open-wa-config] Load Chatwoot as a plugin and place the Chatwoot settings under `pluginConfig.chatwoot`: Install the plugin in the Easy API project first: ```bash npm install @open-wa/integration-chatwoot@5.1.0 export OPENWA_CALLBACK_KEY='replace-with-a-long-random-value' export CHATWOOT_API_ACCESS_TOKEN='replace-with-a-Chatwoot-user-token' ``` ```js const callbackKey = process.env.OPENWA_CALLBACK_KEY?.trim(); const chatwootToken = process.env.CHATWOOT_API_ACCESS_TOKEN; if (!callbackKey || !chatwootToken) throw new Error('Set the Chatwoot and callback secrets'); export default { sessionId: 'sales', port: 8080, host: '0.0.0.0', apiKey: callbackKey, plugins: ['@open-wa/integration-chatwoot'], pluginConfig: { chatwoot: { chatwootUrl: 'https://app.chatwoot.com/api/v1/accounts/123', chatwootApiAccessToken: chatwootToken, apiHost: 'https://wa.example.com', apiKey: callbackKey, forceUpdateCwWebhook: true, }, }, }; ``` The plugin appends `callbackKey` as `api_key` to Chatwoot's webhook URL. The Chatwoot route does not validate it, so configure a reverse proxy or gateway to check the query value before forwarding callbacks. Save this as `wa.config.mjs`, then start the matching session: ```bash npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id sales --host 0.0.0.0 --port 8080 ``` If you want to bind the plugin to an existing Chatwoot inbox, include the inbox ID in `chatwootUrl`: ```ts chatwootUrl: 'https://app.chatwoot.com/api/v1/accounts/123/inboxes/456' ``` `apiHost` must be the public origin that Chatwoot can access. Do not include `/plugins/chatwoot/webhook`. The plugin adds that path. ## Media mapping [#media-mapping] WhatsApp messages become Chatwoot conversation messages. Text messages are sent as normal Chatwoot messages, and media messages are sent as attachments when open-wa can decrypt the file. For WhatsApp to Chatwoot: * `image`, `audio`, `ptt`, `video`, and `document` messages are decrypted and uploaded as Chatwoot attachments when media data is available. * if the WhatsApp message already has a `cloudUrl`, Chatwoot receives a text message containing the file URL and the message text. * location messages become a text message with the location label and a Google Maps link. * button responses use the selected button ID as the message text. For Chatwoot to WhatsApp: * plain Chatwoot replies are sent with `sendText`. * replies containing a URL are sent with link preview support. * replies beginning with a location token like `@51.5072,-0.1276 Meeting point` are sent as WhatsApp locations. * Chatwoot attachments are sent to WhatsApp as images with the Chatwoot message content as the first caption. The plugin marks outbound WhatsApp message IDs as ignored so the same message does not loop back into Chatwoot as a new inbound message. ## Contact sync [#contact-sync] When a WhatsApp message arrives, the plugin looks for a Chatwoot contact with the WhatsApp phone number. If no contact exists and the message includes contact metadata, it creates one. Created contacts use: * the WhatsApp ID as the Chatwoot identifier * the best available display name from WhatsApp contact metadata * the phone number in `+` format * the WhatsApp profile thumbnail URL as `avatar_url` when available * a custom attribute named `wa:number` The plugin keeps an in-memory contact registry and conversation registry while the process is running. For each contact, it reuses an open conversation in the configured inbox when possible, reopens an existing conversation when needed, or creates a new conversation. ## Runbook capture checklist [#runbook-capture-checklist] This guide uses a capture checklist rather than fabricated product screenshots. Chatwoot menus and labels vary across hosted and self-hosted releases, so capture these redacted states from the Chatwoot version you deploy and keep the version beside each image in your private runbook: * **Account and token:** the account URL with the numeric account ID visible, while the access token and personal details are masked. * **API inbox:** the inbox type, name, phone number, and account or inbox ID, with private customer data removed. * **Webhook callback:** the inbox channel configuration showing `/plugins/chatwoot/webhook` and the redacted callback key boundary. * **Incoming message:** an open or reopened conversation containing one redacted WhatsApp message and its timestamp. * **Agent reply:** the same conversation after a plain text reply reaches WhatsApp, with customer and message content masked. * **Attachment path:** one supported attachment state, or the visible file-link result when the inbound media has only `cloudUrl` and cannot be decrypted by open-wa. These captures document the UI state your deployment reached; they do not prove that a different Chatwoot release has the same labels or that webhook authentication is enforced by the open-wa router. Keep the source version, account identifiers, access tokens, phone numbers, and message bodies out of published screenshots. ## Verify the integration [#verify-the-integration] Use these observable checks after configuration. They give you a useful success or failure record without relying on screenshots or asking you to create documentation artifacts. 1. **Confirm the Chatwoot token and account.** Run `curl -i -H "api_access_token: $CHATWOOT_API_ACCESS_TOKEN" "$CHATWOOT_ORIGIN/api/v1/profile"`. Expect `200` and a JSON `account_id`. If you receive `401`, create a new user access token or correct `chatwootUrl`; open-wa cannot create or find an inbox until this request succeeds. 2. **Confirm the open-wa process initialized the integration.** Start the runtime and look for `Chatwoot integration initialized`. If the log instead contains `CW REQ ERROR` or a non-2xx `CW REQUEST`, compare `chatwootUrl`, the token header, and the account/inbox ID parsed from the URL. The plugin uses `api_access_token` on requests to Chatwoot. 3. **Confirm the inbox and callback value.** In Chatwoot, open the API inbox and read its channel webhook URL. It should be `https://wa.example.com/plugins/chatwoot/webhook?api_key=your-secure-key` when `apiHost` and `apiKey` are configured. `forceUpdateCwWebhook: true` makes open-wa PATCH this value during startup; otherwise an existing inbox keeps its current URL. 4. **Understand the callback key boundary.** The plugin appends `api_key` to the URL, and the Easy API's shared API-key middleware accepts `X-API-Key`, `api_key`, or `key` on the API routes. The current Chatwoot Hono router itself parses the query but does not validate `api_key`; if the callback must be authenticated, put a reverse proxy or gateway in front of `/plugins/chatwoot/webhook` that checks the query value (or an equivalent header) before forwarding it. A Chatwoot `200` response alone is not proof that this boundary is protected. 5. **Verify WhatsApp to Chatwoot.** Send a direct WhatsApp message to the connected number, then inspect Chatwoot. Expect a contact, an open or reopened conversation, and an incoming message. The plugin ignores group and broadcast messages; check `message.from`, the session connection state, and the token's contact/conversation permissions when nothing appears. 6. **Verify Chatwoot to WhatsApp.** Send a plain reply from that conversation. Chatwoot must POST an event with `event: "message_created"`, a non-private message, and a sender phone number. Expect the plugin response to contain a WhatsApp message ID and the recipient to receive the text. A private note, incoming message echo, missing phone number, or different event is intentionally ignored. 7. **Verify media and special formats when they matter.** Send one attachment, one URL, and one location-formatted reply. Expect attachments to use `sendImage`, URLs to use link preview, and a leading token such as `@51.5072,-0.1276 Meeting point` to use `sendLocation`. If only `cloudUrl` is available on an inbound media message, expect a file link in Chatwoot rather than an uploaded attachment. ## Troubleshooting [#troubleshooting] * **No inbox appears**: confirm `chatwootUrl` contains the right account ID and that the Chatwoot token can manage inboxes. * **Webhook URL is wrong**: set `apiHost` to the public open-wa origin and set `forceUpdateCwWebhook: true` for one startup. * **Chatwoot replies do not arrive in WhatsApp**: Confirm Chatwoot can POST to `https://wa.example.com/plugins/chatwoot/webhook?api_key=your-secure-key`, then inspect the open-wa log for `Webhook processing error`. The plugin expects `message_created`, a non-private message, a sender phone number, and a connected session. If a reverse proxy checks the query key, make sure its value matches the proxy configuration; the Chatwoot router does not enforce that query value itself. * **WhatsApp messages do not appear in Chatwoot**: Make sure that the WhatsApp session is connected. Make sure that the message is not from a group or broadcast chat. Make sure that the Chatwoot token can create contacts and conversations. * **Media does not appear as an attachment**: confirm open-wa can decrypt the media. If the message has only a `cloudUrl`, Chatwoot receives a file link instead of an uploaded attachment. * **Duplicate messages appear**: check for multiple open-wa runtimes connected to the same WhatsApp account or multiple Chatwoot inboxes pointing to the same webhook. * **Location replies are sent as text**: use the expected location format at the start of the Chatwoot reply, for example `@51.5072,-0.1276 Meeting point`. # Watch WhatsApp from ChatGPT (/docs/client-and-integrations/mcp-events) MCP Events lets ChatGPT Work watch a WhatsApp conversation and act when an update arrives. For example: “Watch this customer conversation. When they ask for a quote, draft a reply using our price list.” ## Enable the endpoint [#enable-the-endpoint] Run Easy API with an API key and MCP enabled: ```json { "apiKey": "your-private-api-key", "mcp": { "enabled": true } } ``` Connect its publicly reachable HTTPS MCP endpoint to your ChatGPT plugin using the Easy API key. The endpoint serves MCP 2.0, protocol revision `2026-07-28`. Event subscriptions are available in ChatGPT Work web chats, desktop Work chats with Cloud selected, and dots. OpenWA advertises these events alongside its tools: | Event | When it arrives | Subscription filter | | ----------------------- | ------------------------------------------------------ | ----------------------------------------------------------------- | | `message.received` | A new incoming message, excluding this account's sends | Optional exact `chatId` | | `ack.changed` | A message acknowledgement changes | Optional exact `chatId`; snapshots without that field won't match | | `session.state.changed` | The linked session changes state | No arguments | Use the conversation ID returned by OpenWA. Ask ChatGPT what to monitor and what it should do; ChatGPT manages `events/subscribe`, refresh and `events/unsubscribe`. ## Payloads and delivery [#payloads-and-delivery] Message updates contain `sessionId`, `messageId`, optional `chatId`, `from` and `body`, plus `truncated`. Message text is capped at 4,000 characters. ChatGPT can use `messages.get` with `messageId` to read the full available message. Acknowledgements contain the message ID, acknowledgement value and available conversation ID. State updates contain the current and available previous state. OpenWA verifies the callback before sending WhatsApp data. It signs each request using the secret supplied by ChatGPT, validates public HTTPS destinations for each connection and rejects redirects. A successful HTTP response acknowledges receipt; ChatGPT processes the event asynchronously. Transient failures receive bounded retries, with the same event ID. `410` and `413` responses aren't retried. Deliveries can repeat or arrive out of order. Subscriptions and pending deliveries persist in a private SQLite file under `.open-wa/`. Keep that directory on persistent storage when running in a container. Events missed while OpenWA is offline aren't replayed: `cursor` is always `null`. ## Subscription settings [#subscription-settings] ```json { "mcp": { "enabled": true, "events": { "enabled": true, "storePath": ".open-wa/mcp-events.sqlite", "lifetimeMs": 86400000 } } } ``` The default granted lifetime is one day. OpenWA returns `refreshBefore` so ChatGPT can renew it. A shorter requested lifetime is honored. A request for no expiration still receives a finite lifetime. Set `mcp.events.enabled` to `false` to expose tools without subscriptions. [Read the generated event contracts](/docs/reference/event-contracts) or [download AsyncAPI](/asyncapi.json). The OpenAI MCP Extensions SDK provides sidebar views, file handlers, mentions and forms. This integration uses the standard event subscription methods and doesn't require that UI SDK. [OpenAI MCP Events requirements](https://developers.openai.com/plugins/build/mcp-events), [OpenAI MCP Extensions](https://github.com/openai/mcp-extensions). # Proxying a session (/docs/client-and-integrations/proxying-a-session) # Proxying a session [#proxying-a-session] The Easy API starts Chromium with the browser arguments in `chromiumArgs`. Use Chromium's `--proxy-server` option when the session must use an outbound proxy. The current runtime does not apply proxy username/password credentials from `proxyServerCredentials`; use a proxy that allows the runtime host's IP or another network-level authentication method. ## Configure an HTTP proxy [#configure-an-http-proxy] Create `wa.config.mjs` with the proxy address supplied by your provider: ```js export default { sessionId: 'sales', host: '127.0.0.1', port: 8080, chromiumArgs: ['--proxy-server=http://proxy.example:1234'], }; ``` Start Easy API with the same session and address values: ```bash npx @open-wa/wa-automate@5.1.0 \ --config ./wa.config.mjs \ --session-id sales \ --host 127.0.0.1 \ --port 8080 ``` The CLI flags make the selected session and listener explicit. Keep the proxy hostname reachable from the machine or container running Chromium. For a SOCKS proxy, pass the scheme and endpoint your provider supports, such as `--proxy-server=socks5://proxy.example:1080`. ## Check the connection [#check-the-connection] From the runtime host, check that the proxy accepts a request: ```bash curl -x 'http://proxy.example:1234' https://api.ipify.org ``` This checks the proxy route from the shell; it does not confirm Chromium used the same route. After the session starts, complete authentication and exercise the browser paths your application depends on, including reconnect and media transfer. Review the runtime logs for navigation, WebSocket, or network errors. ## Authentication limits [#authentication-limits] The `proxyServerCredentials` config shape is present in the configuration schema, but the current runtime does not wire its username or password into Chromium. Do not put credentials in `--proxy-server` URLs or checked-in config. Use a provider that supports source-IP allowlisting, a proxy reachable only on a protected network, or an authentication mechanism configured outside this runtime. ## Related [#related] * [Configuration and CLI](/docs/guides/configuration-and-cli) * [Cloudflare Session Proxy](/docs/client-and-integrations/cf-proxy) for remote access to a running session without an inbound port * [Security and deployment](/docs/operations/security-and-deployment) # Socket Client (/docs/client-and-integrations/socket-client) # Socket Client [#socket-client] Use `@open-wa/socket-client` in a separate Node.js app when Easy API owns the WhatsApp browser session. In the current v5 runtime, commands use HTTP RPC and events use Server-Sent Events (SSE); the package keeps the familiar `Client` method and listener surface. ## Install and connect [#install-and-connect] Install the client and `tsx` in the Node.js app that will consume Easy API: ```bash npm install @open-wa/socket-client@5.1.0 npm install --save-dev tsx ``` Start or locate an Easy API session, then connect using its base URL. Pass the API key when the server requires one: ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://127.0.0.1:8080', process.env.OPENWA_API_KEY); ``` Keep the key in server-side configuration. The client sends it as `X-API-Key` on command requests and as `api_key` on the SSE URL because the standard `EventSource` API cannot set custom headers. ## Run a consumer [#run-a-consumer] Save this as `consumer.ts`. It logs incoming messages and does not send a WhatsApp message: ```ts import { SocketClient } from '@open-wa/socket-client'; const url = process.env.OPENWA_API_URL; const apiKey = process.env.OPENWA_API_KEY; if (!url) throw new Error('Set OPENWA_API_URL to the Easy API base URL'); const client = await SocketClient.connect(url, apiKey); client.socket.on('connect', () => console.log('Easy API event stream connected')); client.socket.on('disconnect', (reason) => console.warn('Event stream disconnected:', reason)); client.socket.on('connect_error', (error) => console.error('Event stream connection failed:', error)); await client.listen('onMessage', (message) => { console.log('Incoming message:', message.from, message.body); }); console.log('Listening for messages; press Ctrl-C to stop'); process.once('SIGTERM', () => { client.close(); process.exit(0); }); ``` Run it with the address of your Easy API instance: ```bash OPENWA_API_URL='http://127.0.0.1:8080' npx tsx consumer.ts ``` If Easy API requires an API key, add `OPENWA_API_KEY='your-key'` to the command. For any example that calls `sendText`, replace the destination with a chat you control and run it only when you intend to send that message. ## Call commands and listen for events [#call-commands-and-listen-for-events] The client exposes Easy API methods directly, and `ask()` is available when you prefer an explicit RPC call: ```ts const result = await client.sendText('447700900123@c.us', 'Hello from SocketClient'); // Equivalent explicit command: const sameResult = await client.ask('sendText', { to: '447700900123@c.us', content: 'Hello from SocketClient', }); ``` `onMessage` can also be registered through the listener helper: ```ts await client.listen('onMessage', (message) => console.log(message.body)); ``` Use a chat ID for a chat you control before running the send example. The `client.socket` event hooks report the SSE connection; they do not indicate whether an individual command succeeded. ## Reconnect and shutdown [#reconnect-and-shutdown] `SocketClient.connect()` waits for the first SSE connection before resolving. The underlying event source retries a dropped stream; listeners are registered again after it reconnects. HTTP command calls can fail independently, so handle command errors at the call site. If you need to initiate a reconnect yourself, call `reconnect()`. Use `close()` or `disconnect()` when stopping the consumer: ```ts try { await client.reconnect(); } catch (error) { console.error('Could not reconnect:', error); } client.close(); ``` If the first stream connection fails, `connect()` rejects with `Unable to establish SSE connection`. A stopped event stream does not by itself mean the WhatsApp session stopped; check Easy API health before restarting its runtime. ## Cloudflare Session Proxy [#cloudflare-session-proxy] For remote access without opening an inbound port to Easy API, use the [Cloudflare Session Proxy](/docs/client-and-integrations/cf-proxy). `SocketClient.connect()` accepts its `cf-proxy://` URL format. ## Compatibility and security [#compatibility-and-security] This page covers the v5 HTTP RPC and SSE transport. Older v4 deployments and socket.io examples use a different transport; keep the client package aligned with the Easy API release you operate. The consumer can call methods exposed by Easy API and receive runtime events. It cannot inspect the browser page or run browser-side code. Treat the API key as a server secret and restrict network access to the API. ## Related [#related] * [Easy API quick start](/docs/getting-started/easy-api) * [Configuration and CLI](/docs/guides/configuration-and-cli) * [Webhook payloads](/docs/client-and-integrations/webhook-payloads) # Webhook payloads (/docs/client-and-integrations/webhook-payloads) # Webhook payloads [#webhook-payloads] [Read the generated webhook event contracts](/docs/reference/event-contracts) or [download AsyncAPI 3.1](/asyncapi.json) for the wire envelope and payload schemas. This page is the reference for the v5 webhook integration. It shows the envelope, a bounded authenticated receiver, and the delivery limits that apply to the default configuration. The CLI still parses `--webhook`, but it does not register webhook delivery. Load `@open-wa/integration-webhook` from `wa.config.mjs` and configure it under `pluginConfig.webhook`. ## Enable delivery [#enable-delivery] Install the package in the project that runs the Easy API, then create `wa.config.mjs`: ```bash npm install @open-wa/integration-webhook@5.1.0 ``` ```js // wa.config.mjs const webhookSecret = process.env.OPEN_WA_WEBHOOK_SECRET?.trim(); if (!webhookSecret) throw new Error('OPEN_WA_WEBHOOK_SECRET is required'); export default { sessionId: 'sales', host: '127.0.0.1', port: 8080, plugins: ['@open-wa/integration-webhook'], pluginConfig: { webhook: { url: 'https://receiver.example/webhooks/open-wa', events: ['message.received', 'session.state.changed'], headers: { 'X-Webhook-Secret': webhookSecret }, concurrency: 10, queueCapacity: 1000, overload: 'backpressure', retries: 3, retryDelay: 1000, timeout: 30000, // Set enabled: true only when this process can write the journal path. durability: { enabled: false, path: '.openwa/webhook-deliveries.sqlite', replayLimit: 1000, }, }, }, }; ``` Start the CLI from the directory containing the config: ```bash export OPEN_WA_WEBHOOK_SECRET='use-a-long-random-value' npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id sales --host 127.0.0.1 --port 8080 ``` The receiver must be listening before open-wa starts sending events. A successful delivery is any HTTP `2xx` response; return it after storing the event or its deduplication key, then do slower work asynchronously. ## Envelope [#envelope] Every delivery has the same outer fields. `payload` is the event-specific value; current v5 does not use the older `data` field. ```json { "webhookId": "0f5d6a52-2ff2-43d5-9d0d-9c0c8dd5f9d1", "sessionId": "sales", "event": "message.received", "payload": { "message": { "id": "false_1234567890@c.us_ABC123", "from": "1234567890@c.us", "to": "0987654321@c.us", "body": "Hello, I need help with my order", "caption": "", "type": "chat", "timestamp": 1700000000, "fromMe": false, "isGroupMsg": false, "isMedia": false } }, "timestamp": 1700000000123 } ``` Store only the envelope fields and payload data required for the receiving workflow. `webhookId` identifies the plugin instance for that process; it is shared across that instance's events, so don't use it as a per-event deduplication key. For message workflows, the first useful application fields are: | Application field | v5 path | Use | | ----------------- | --------------------------------------------------- | ----------------------------------- | | Sender | `payload.message.from` | Route to a contact or conversation. | | Text | `payload.message.body` or `payload.message.caption` | Handle text and media captions. | | Message ID | `payload.message.id` | Build an idempotency key. | ## Canonical receiver [#canonical-receiver] This receiver rejects a missing secret at startup, authenticates before parsing the body, limits JSON to 2 MiB, returns explicit errors for malformed input, and logs identifiers rather than message content. It is the same receiver recipe used in [Webhooks for Business](/docs/guides/webhooks-for-business). ```js // receiver.mjs import express from 'express'; import Database from 'better-sqlite3'; import { createHash, timingSafeEqual } from 'node:crypto'; const expectedSecret = process.env.OPEN_WA_WEBHOOK_SECRET?.trim(); if (!expectedSecret) { throw new Error('OPEN_WA_WEBHOOK_SECRET must be set before the receiver starts'); } const expectedBytes = Buffer.from(expectedSecret, 'utf8'); const inbox = new Database('./openwa-webhook-inbox.sqlite'); inbox.exec(` CREATE TABLE IF NOT EXISTS webhook_inbox ( idempotency_key TEXT PRIMARY KEY, session_id TEXT NOT NULL, event TEXT NOT NULL, received_at INTEGER NOT NULL, envelope TEXT NOT NULL ) `); const insertInbox = inbox.prepare(` INSERT OR IGNORE INTO webhook_inbox (idempotency_key, session_id, event, received_at, envelope) VALUES (?, ?, ?, ?, ?) `); const app = express(); const rawBodies = new WeakMap(); const allowRawDiagnostics = process.env.NODE_ENV !== 'production' && process.env.DEBUG_WEBHOOK_RAW === '1'; function authenticate(req, res, next) { const supplied = req.get('X-Webhook-Secret'); if (!supplied || supplied.length !== expectedSecret.length) { return res.status(401).send('Unauthorized'); } const suppliedBytes = Buffer.from(supplied, 'utf8'); if (suppliedBytes.length !== expectedBytes.length || !timingSafeEqual(suppliedBytes, expectedBytes)) { return res.status(401).send('Unauthorized'); } next(); } function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function parseEnvelope(value) { if (!isRecord(value)) return null; if (typeof value.webhookId !== 'string' || !value.webhookId) return null; if (typeof value.sessionId !== 'string' || !value.sessionId) return null; if (typeof value.event !== 'string' || !value.event) return null; if (typeof value.timestamp !== 'number' || !Number.isFinite(value.timestamp)) return null; if (!('payload' in value)) return null; return value; } app.post('/webhooks/open-wa', authenticate, express.json({ limit: '2mb', // Temporary local diagnosis only. This is disabled when NODE_ENV is production. verify: (req, _res, buffer) => { if (allowRawDiagnostics) rawBodies.set(req, buffer.toString('utf8')); }, }), (req, res) => { const envelope = parseEnvelope(req.body); if (!envelope) return res.status(400).send('Invalid open-wa webhook envelope'); if (allowRawDiagnostics) { console.warn('[LOCAL ONLY] raw webhook body:', rawBodies.get(req) ?? '(empty body)'); } const messageId = envelope.payload?.message?.id; const idempotencyKey = req.get('Idempotency-Key') || (typeof messageId === 'string' && messageId ? `${envelope.sessionId}:${envelope.event}:${messageId}` : createHash('sha256').update(JSON.stringify(envelope)).digest('hex')); // Insert the authenticated event before acknowledging it. A duplicate key is a no-op. const result = insertInbox.run( idempotencyKey, envelope.sessionId, envelope.event, Date.now(), JSON.stringify(envelope), ); console.info('webhook inbox updated', { sessionId: envelope.sessionId, event: envelope.event, duplicate: result.changes === 0, }); res.sendStatus(204); }); app.use((error, _req, res, _next) => { if (error?.type === 'entity.too.large') return res.status(413).send('Webhook body too large'); if (error?.type === 'entity.parse.failed') return res.status(400).send('Malformed JSON'); console.error('webhook receiver error', { name: error?.name ?? 'Error' }); res.status(500).send('Webhook receiver error'); }); app.listen(3000, () => console.log('Webhook receiver listening on http://localhost:3000')); ``` Install and run the receiver in its own project: ```bash npm install express better-sqlite3 export OPEN_WA_WEBHOOK_SECRET='use-a-long-random-value' node receiver.mjs ``` The following requests exercise the rejection paths before you connect WhatsApp: ```bash # Missing secret: HTTP 401 curl -i -X POST http://localhost:3000/webhooks/open-wa \ -H 'Content-Type: application/json' \ -d '{}' # Authenticated but malformed JSON: HTTP 400 curl -i -X POST http://localhost:3000/webhooks/open-wa \ -H 'Content-Type: application/json' \ -H "X-Webhook-Secret: $OPEN_WA_WEBHOOK_SECRET" \ -d '{' # Valid envelope: HTTP 204 curl -i -X POST http://localhost:3000/webhooks/open-wa \ -H 'Content-Type: application/json' \ -H "X-Webhook-Secret: $OPEN_WA_WEBHOOK_SECRET" \ -d '{"webhookId":"local-test","sessionId":"sales","event":"message.received","payload":{"message":{"id":"local-1","from":"1234567890@c.us","body":"Hello"}},"timestamp":1700000000123}' ``` The current plugin sends configured headers as-is; it does not generate an HMAC signature. With `durability.enabled: true`, it also sends an `Idempotency-Key` for the durable delivery row. If a gateway adds an HMAC header, verify the raw body before parsing and reject a signature whose decoded length differs from the expected digest before calling `timingSafeEqual`. ## Delivery limits and recovery [#delivery-limits-and-recovery] The defaults are finite and bounded: | Behavior | Default | | ------------------------------- | ---------------------------------- | | Concurrent requests | `10` | | In-memory waiting queue | `1000` deliveries | | Full queue | `backpressure` (wait for capacity) | | Request timeout | `30000ms` | | Retries after the first request | `3` | | Retry delays | `1000ms`, `2000ms`, `4000ms` | Network errors, timeouts, and non-`2xx` responses consume attempts. After the final attempt the event is logged as failed and the default in-memory configuration keeps no record for restart or automatic replay. This is a finite retry policy, not a guarantee of eventual delivery. Set `durability.enabled: true` to journal queued deliveries in SQLite. Pending rows are replayed on a later startup up to `replayLimit`; rows that exhaust retries are marked dead letter and the integration has no built-in inspection or replay route. The journal improves restart recovery but still permits duplicates, so the receiver should insert the supplied `Idempotency-Key` into its inbox before responding. When durability is disabled, this example derives a message key from `sessionId:event:payload.message.id` or hashes the exact envelope for other events. ## Common event values [#common-event-values] | Event | Payload focus | | ----------------------------------- | ----------------------------------------------------------------- | | `message.received` | `payload.message` for an inbound message. | | `message.any` | `payload.message` for inbound or outbound messages. | | `message.deleted` | `payload.messageId`, `payload.chatId`, and optional `payload.by`. | | `ack.changed` | `payload.ack` acknowledgment details. | | `session.state.changed` | `payload.details.prev` and `payload.details.next`. | | `group.participants.changed.global` | `payload.change` participant changes. | ## Troubleshooting [#troubleshooting] | Problem | Cause | Fix | | -------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | The receiver gets no requests | The integration is not in `plugins`, or the event is not in `events`. | Add `@open-wa/integration-webhook` to `plugins`. Add the event name to `events`. | | The log shows connection errors | The receiver was not listening when open-wa started. | Start the receiver first. Check that open-wa can connect to the `url` value. | | The receiver returns `401` | The `X-Webhook-Secret` value does not match. | Use the same secret in the sender and in the receiver. | | The same event arrives two times | A timeout or a non-`2xx` response caused a retry. | Return `2xx` quickly. Store the `Idempotency-Key` value or the message ID, and ignore duplicates. | | Events stop after a restart | The in-memory queue does not survive a restart. | Set `durability.enabled: true` to journal deliveries in SQLite. | For complete receivers, read [webhooks for business](/docs/guides/webhooks-for-business) and [build a WhatsApp bot in Python](/docs/guides/whatsapp-bot-python). ## Generated type reference [#generated-type-reference] # Data models (/docs/concepts/data-models) # Data models [#data-models] These are the core runtime models that show up across the client surface, listeners, webhooks, and generated reference pages. Read the task-oriented guides first when you are learning the workflow. Use this page when you need the exact object shape behind messages, chats, contacts, and group metadata. ## Chat ID narrative [#chat-id-narrative] Every message operation starts with a chat ID. For a direct chat, take the phone number in international format, remove formatting, and add the `@c.us` suffix. ```text +44 7700 900000 447700900000 447700900000@c.us ``` Groups use the `@g.us` suffix. You do not build a group chat ID from a phone number. WhatsApp assigns it when the group is created, and you read it from an incoming group message, a group listing, or resolved invite metadata. ```text 120363025814912345@g.us ``` LIDs are internal linked device identifiers. They can appear in sender, participant, or lower-level payload fields when WhatsApp needs an internal identity rather than the plain contact chat ID. Treat them as identifiers to preserve and compare, not as phone numbers to format yourself. The usual conversion flow for a direct chat is: ```text phone number with country code plain digits only digits plus @c.us chatId used by send, reply, contact, and webhook payloads ``` ## Message direction [#message-direction] Use `fromMe` to tell whether the current session sent the message. When `fromMe` is `false`, the message is incoming. When `fromMe` is `true`, the message is outgoing. The `from` and `to` fields still matter. On an incoming one-to-one message, `from` is the contact chat ID and `to` is your session. On an outgoing message, `from` is usually your session and `to` is the recipient chat ID. For group messages, `from` or `chatId` is the group chat ID, while `author` or `senderId` can identify the participant who wrote the message. The `self` field gives the same direction in string form: `in` for incoming and `out` for outgoing. Prefer `fromMe` when you need a boolean check, and use `self` when you are logging or storing direction labels. ## Message [#message] Messages carry the event data you usually handle in listeners and webhooks. The generated table below is the contract, and the notes here explain the fields that are easiest to mix up in production. ### Media fields [#media-fields] Media messages still arrive as messages. The `type` field tells you the broad message kind, such as `image`, `video`, `audio`, `ptt`, `document`, or `sticker`. The `isMedia` flag is the quick check before you try to decrypt or store binary content. Common media fields include: | Field | Meaning | | --------------------------------- | ------------------------------------------------------------------------------- | | `type` | WhatsApp message type, such as `image`, `video`, or `document`. | | `mimetype` | MIME type for the encrypted media, such as `image/jpeg` or `application/pdf`. | | `caption` | Optional text attached to image, video, or document messages. | | `mediaData` | Runtime metadata used to locate and decrypt the encrypted media. | | `clientUrl` / `deprecatedMms3Url` | Media URLs that can be present on some payloads. Treat them as runtime details. | To access the raw bytes, pass the whole message to `decryptMedia` after you have checked that the message has media metadata. ```ts import { decryptMedia } from '@open-wa/wa-automate'; client.onMessage(async (message) => { if (!message.isMedia && !message.mimetype) return; const media = await decryptMedia(message); // Save, inspect, or forward the decrypted data from here. }); ``` Do not assume `body` contains file bytes. For media, read `caption` for user text and decrypt the message when you need the content. ### Nullability patterns [#nullability-patterns] Most IDs and timestamps are expected to be present because they are needed to route and order events. Optional content fields depend on the message type and on how much data WhatsApp exposed for that event. Common patterns: | Field | When it can be missing or null | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `body` | Can be empty or null for media-only, notification, revoked, or system-style messages. | | `caption` | Only appears when the sender included media caption text. | | `quotedMsg` / `quotedMsgObj` | Only appears when the quoted message is still available to the runtime. These are currently **untyped** in the schema (`z.any()`) — the runtime returns a nested message-shaped object, but the generated types do not describe its fields, so treat them as `unknown` and narrow before use. | | `author` / `senderId` | Most useful in group messages. It can be absent in one-to-one messages. | | `notifyName` | Depends on whether WhatsApp has a display name for the sender. | | `mimetype` / `mediaData` | Only appears for media messages that have decryptable media metadata. | When storing messages, model optional fields as optional or nullable even when your current payload sample includes them. WhatsApp payloads vary by message type, session state, and runtime version. ### Sample payloads [#sample-payloads] A simple incoming text message has a contact chat ID in `from`, your session in `to`, and `fromMe: false`. ```json { "id": "false_447700900000@c.us_3EB0A1B2C3D4E5F67890", "body": "Hi, is my order ready?", "type": "chat", "t": 1768848000, "timestamp": 1768848000, "from": "447700900000@c.us", "to": "15551234567@c.us", "chatId": "447700900000@c.us", "fromMe": false, "self": "in", "isGroupMsg": false, "isMedia": false, "isNotification": false, "ack": 1, "sender": { "id": "447700900000@c.us", "pushname": "Aisha", "isMyContact": true, "isWAContact": true } } ``` An outgoing group message uses the group chat ID as the conversation target and sets `fromMe: true`. ```json { "id": "true_120363025814912345@g.us_3EB0F00D112233445566", "body": "I will post the notes here.", "type": "chat", "t": 1768848120, "timestamp": 1768848120, "from": "15551234567@c.us", "to": "120363025814912345@g.us", "chatId": "120363025814912345@g.us", "fromMe": true, "self": "out", "isGroupMsg": true, "isMedia": false, "isNotification": false, "ack": 2, "author": "15551234567@c.us" } ``` A media message can have no useful `body`. Use `caption` for attached text and `mimetype` plus `mediaData` for decryption. ```json { "id": "false_447700900000@c.us_3EB09988776655443322", "body": null, "caption": "Receipt from today", "type": "image", "mimetype": "image/jpeg", "t": 1768848240, "timestamp": 1768848240, "from": "447700900000@c.us", "to": "15551234567@c.us", "chatId": "447700900000@c.us", "fromMe": false, "self": "in", "isGroupMsg": false, "isMedia": true, "isMMS": true, "isNotification": false, "mediaData": { "type": "image", "mediaStage": "RESOLVED", "mimetype": "image/jpeg", "filehash": "8f2b5f7b4c0a1d2e3f4a5b6c7d8e9f00" } } ``` ## Contact [#contact] Contact objects describe a person or WhatsApp identity. The stable field is `id`, which is the contact chat ID. Display fields are optional because the runtime can know a saved name, a push name, the two names, or neither name. Common contact fields include `name`, `shortName`, `pushname`, `formattedName`, `isBusiness`, `isEnterprise`, `isMe`, `isMyContact`, `isUser`, `isWAContact`, `labels`, `profilePicThumbObj`, `verifiedName`, `isOnline`, and `lastSeen`. Treat contact display data as helpful context, not as identity. Use `id` for lookups, message targets, and deduplication. ## Chat [#chat] A chat object describes the conversation container. For a one-to-one chat, `id` points at a contact chat ID and `isGroup` is `false`. For a group chat, `id` uses `@g.us`, `isGroup` is `true`, and `groupMetadata` can be present. Common chat fields include `id`, `name`, `formattedTitle`, `isGroup`, `contact`, `groupMetadata`, `presence`, `unreadCount`, `lastReceivedKey`, `msgs`, `isReadOnly`, `muteExpiration`, `pin`, and `ack`. Some chat fields are snapshots. For example, `unreadCount`, `presence`, and `lastReceivedKey` can change between events. Read them as current runtime context, not permanent facts. ## Group metadata [#group-metadata] Group metadata describes the group itself rather than a single message. The necessary fields are `id`, `subject`, `creation`, and `participants`. Admin and configuration fields are optional because they depend on what the session can see and what WhatsApp has exposed for that group. Common group fields include `owner`, `desc`, `descTime`, `descOwner`, `restrict`, `announce`, and `participants`. Each participant has an `id`, an `isAdmin` flag, and can include `isSuperAdmin`. Use the group `id` as the chat ID for sending messages. Use participant IDs when mentioning, moderating, or showing who sent a group message. # Glossary (/docs/concepts/glossary) # Glossary [#glossary] ## ChatId [#chatid] A `ChatId` is the identifier accepted by a chat or messaging method. The current schema accepts a numeric direct-contact JID such as `447700900000@c.us`, a numeric group JID such as `447123456789-1445627445@g.us`, or a numeric LID JID such as `12345678901234@lid`. An incoming event can expose the value through fields such as `from` and `chatId`; preserve that returned value instead of reconstructing it. ## ContactId [#contactid] A `ContactId` is a chat ID for a direct contact. The schema requires the numeric `@c.us` form, such as `447700900000@c.us`; display names, invite URLs, and guessed LIDs are not contact IDs. ## Driver [#driver] A driver is the browser automation backend open-wa uses to control WhatsApp Web. Common drivers include Puppeteer, Playwright, and Lightpanda. ## Easy API [#easy-api] The HTTP and runtime-transport surface served by an open-wa process. Running `npx @open-wa/wa-automate` starts it locally; the process can expose client methods, interactive API docs, and generated schemas. ## Event namespace [#event-namespace] An event namespace is the naming convention used for runtime events, such as `message.received`, `client.ready`, and `core.stopping`. ## GroupChatId [#groupchatid] A `GroupChatId` is an assigned WhatsApp group identifier ending in `@g.us`. Keep the full value returned by an event or group method; an invite URL is an invite token and does not become a group ID until a group method resolves it. ## Headless / headful [#headless--headful] `headless` means the browser runs without a visible UI. `headful` means the browser window is visible for debugging or manual observation. ## Host account [#host-account] The WhatsApp account that authenticates and owns the session. ## License tiers [#license-tiers] License tiers are the different licensing levels that gate certain features, such as free, insiders, and restricted. ## LID [#lid] A LID is an opaque WhatsApp identifier ending in `@lid`. The current API schema accepts a numeric value before that suffix. Keep the ID supplied by the runtime; don't derive one from a phone number or assume every incoming LID has the same representation. The [Chat ID primer](/docs/guides/chatid-primer) explains which inputs the current API accepts. ## MCP [#mcp] MCP means Model Context Protocol. It is a standard for connecting AI assistants to external tools. When MCP is enabled for an Easy API instance and configured with its API key, open-wa exposes Easy API methods as MCP tools. ## Proxy [#proxy] A proxy is a middleman service that forwards traffic between your app and WhatsApp. In open-wa contexts, this can include Cloudflare Session Proxy and SOCKS or HTTP proxies. ## Re-auth [#re-auth] Re-auth is the process of bringing an existing session back online without a fresh QR scan when the necessary auth state is still usable. ## Re-scan [#re-scan] A re-scan is the user action of scanning a fresh QR code after a session can no longer be restored automatically. ## Session [#session] A session is a named automation instance. The `sessionId` is the stable identifier you use to distinguish one runtime from another. ## Session data [#session-data] Session data is the authentication material that restores a session. Its portability depends on the auth/runtime mode that you use. ## SocketClient [#socketclient] The remote consumer client used to connect to an Easy API instance without embedding the full browser automation runtime in your own app. ## Tunnel [#tunnel] A tunnel, such as Cloudflare Tunnel, exposes a local service through a public relay URL without requiring an inbound listening port on your machine or network. The relay URL is still an internet-facing entry point, so apply the service's authentication and access controls. ## WAPI [#wapi] WAPI is the internal WhatsApp Web API that open-wa bridges to. It is the abstraction layer between the browser runtime and WhatsApp Web. ## Webhook [#webhook] A webhook is an HTTP POST notification sent to your endpoint when WhatsApp events occur. # How it works (/docs/concepts/how-it-works) # How it works [#how-it-works] This page explains the processes that operate before your code can send messages or receive events. It also explains which process owns the browser session. Use this information to select Easy API, custom code, or a remote consumer. ## Runtime flow [#runtime-flow] The same runtime can be reached directly from your app, through the Easy API, or through remote consumers: ```text Browser session WhatsApp Web | v Runtime bridge injected by open-wa | v Client surface actions, listeners, lifecycle, events | +--> Library mode | your Node.js process calls createClient() | +--> Easy API | HTTP commands, generated docs, webhooks, MCP | +--> Remote consumers SocketClient, Cloudflare proxy, external services ``` Every integration reaches the same browser-backed client. The process that starts the browser owns the session; API and remote consumers send commands to that process. ## Browser injection [#browser-injection] open-wa starts or connects to a browser page, loads WhatsApp Web, then registers the bridge code inside that page. The bridge gives the Node.js side a controlled way to call WhatsApp Web capabilities and listen for runtime events. Injection is treated as part of session readiness, not just a setup detail. The runtime checks that the bridge, store, and necessary methods are present before the client is considered ready. If a navigation or reload removes the bridge, the runtime can detect that and run a reinjection path before exposing the session again. ## API/Socket/MCP flow [#apisocketmcp-flow] The public surfaces sit on top of the same client surface: * **Easy API** receives HTTP requests, checks lifecycle readiness, and calls the matching client method. * **SocketClient** connects to a running Easy API instance, sends commands over the remote API path, and listens for runtime events from that process. * **MCP** exposes Easy API methods as tools for AI agents. Tool execution is blocked until the session is connected and ready. That means remote consumers do not own the browser. They talk to the process that owns the browser. ## Lifecycle states [#lifecycle-states] A session usually moves through these operational states: 1. **starting**: the runtime is resolving config, opening the browser, and preparing the page 2. **QR**: the browser needs authentication and a QR code or link-code flow is available 3. **authenticated**: WhatsApp Web has accepted the login, but the runtime can still be syncing, injecting, or validating 4. **ready**: the bridge is usable and client methods can run safely 5. **stopping**: shutdown has started and listeners, queues, plugins, and browser resources are being drained 6. **stopped**: the process has released the browser/runtime resources for that session In code, some of these are emitted through lower-level names such as `STARTING`, `AUTHENTICATING`, `READY`, and `STOPPED`. The docs use the practical names above because they match what operators see during startup and shutdown. ## Session-data storage [#session-data-storage] Modern session persistence uses a browser profile. Unless you select `ephemeral`, open-wa resolves a `userDataDir` for the browser profile. If you do not provide one, the runtime derives a directory from the session ID. It usually uses `./_IGNORE_` relative to `sessionDataPath` or the working directory. That profile contains the browser state needed for WhatsApp Web to restore the login after restart. If you provide `userDataDir`, the runtime uses your path instead. Legacy JSON session restore still exists for migration. In that path, open-wa looks for `.data.json` under `sessionDataPath`, or treats `sessionDataPath` as the full file path if it already ends in `.data.json`. On logout cleanup, that file can be marked `LOGGED OUT` or deleted, and the `userDataDir` can also be removed when configured. The CLI can also create `.open-wa/chrome-executable-path.json` to cache the resolved Chrome executable path. That file is runtime convenience data, not WhatsApp authentication state. ## Where plugins fit [#where-plugins-fit] Plugins sit after the core event emitter and before external integrations: ```text Runtime events | v HyperEmitter | v PluginHost | +--> plugin hooks +--> plugin routes +--> plugin dashboard pages +--> plugin tools | v External services webhooks, Chatwoot, S3, custom integrations ``` This keeps the browser and client lifecycle inside open-wa, while plugins extend what happens around events, routes, tools, and integration output. ## Security-filtered event pipeline [#security-filtered-event-pipeline] Plugins do not receive the raw internal event bus. `PluginHost` creates an event gateway for each plugin, and that gateway only allows subscriptions to public, non-sensitive events. Internal launch, patch, persistence, webhook delivery, server, and license events are blocked from plugin event subscriptions. Sensitive authentication events, such as QR and link-code payloads, are also blocked. Plugins can listen to public message, ack, session, group, chat, device, call, UI, label, story, commerce, reaction, core, and error events. The gateway also does not expose `emit()`. Plugins consume events and provide hooks, routes, pages, or tools, but they do not get a direct write handle to the core event bus. ## The three primary usage modes [#the-three-primary-usage-modes] You call `createClient()` directly and own the client in your application process. You run the runtime as an API surface, then consume it remotely over HTTP and related transports. You connect to an already running Easy API instance with `SocketClient` instead of embedding the browser automation runtime in your own app. ## What to do next [#what-to-do-next] * Start with **Easy API mode** when you need a running WhatsApp-backed HTTP API quickly. * Use **Library mode** when your Node.js app must own browser startup, lifecycle, and event handling directly. * Use **Remote-consumer mode** when another service must communicate with an Easy API process that owns the browser session. For each path, make sure that the session gets to **ready** before you send commands. Keep session data in durable storage when restarts are necessary. Protect all remote surfaces with API keys or proxy tokens. # Choose a package (/docs/concepts/packages) Start with the package that matches where your WhatsApp session will run. You can add integrations and lower-level packages when your application needs them. | What you want to do | Package and setup guide | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | Run a local HTTP API | [`@open-wa/wa-automate`: Easy API quick start](/docs/getting-started/quickstart) | | Own the browser and session inside a Node.js app | [`@open-wa/wa-automate`: custom code](/docs/getting-started/custom-code) | | Connect to an Easy API session that is already running | [`@open-wa/socket-client`: remote client setup](/docs/client-and-integrations/socket-client) | | Build a plugin | [`@open-wa/plugin-sdk`: plugin getting started](/docs/plugins/getting-started) | | Decrypt downloaded WhatsApp media | [`@open-wa/decrypt`: package reference](/docs/reference/workspaces/packages/decrypt) | The installation examples use **5.1.0**. See [release status](/docs/releases/v5-alpha) before choosing a different npm tag or Docker image; those release channels can move independently. The [v5.1.0 release notes](/docs/releases/v5.1) describe changes in this release. ## Choose a browser driver [#choose-a-browser-driver] Embedded applications pass a driver to `createClient()`. The [custom-code guide](/docs/getting-started/custom-code) includes a complete Puppeteer example with installation, authentication, message handling, and shutdown. | Driver package | When to consider it | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [`@open-wa/driver-puppeteer`](/docs/reference/workspaces/packages/driver-puppeteer) | Follow the Puppeteer setup used in the getting-started examples. | | [`@open-wa/driver-playwright`](/docs/reference/workspaces/packages/driver-playwright) | Use the Playwright adapter when it fits your browser setup. | | [`@open-wa/driver-lightpanda`](/docs/reference/workspaces/packages/driver-lightpanda) | Explore the experimental Lightpanda adapter and its current limitations. | ## Explore the implementation [#explore-the-implementation] Most applications can stay with the entry points above. If you need to work on the runtime itself, the package references describe each workspace: * [`@open-wa/client`](/docs/reference/workspaces/packages/client) exposes the Client API. * [`@open-wa/core`](/docs/reference/workspaces/packages/core) coordinates the browser driver and runtime events. * [`@open-wa/config`](/docs/reference/workspaces/packages/config) loads and validates configuration. * [`@open-wa/session-sync`](/docs/reference/workspaces/packages/session-sync) handles session synchronization. * [`@open-wa/screencaster`](/docs/reference/workspaces/packages/screencaster) streams the browser screen to a client. * [`@open-wa/hyperemitter`](/docs/reference/workspaces/packages/hyperemitter) provides the runtime's schema-based event emitter. Browse the [workspace reference](/docs/reference/workspaces) for the complete package, application, and integration list. # SDK quick start (/docs/getting-started/custom-code) # SDK quick start [#sdk-quick-start] Use the SDK when your application owns the browser and session. Use **Node.js 22.21.1 or newer** and npm. ## Create the bot [#create-the-bot] ```bash npm init @open-wa@latest my-bot -- --mode sdk cd my-bot npm start ``` Choose JavaScript or TypeScript, your browser, and any plugins in the wizard. You can also use the [project builder](/new?mode=sdk) to preview the generated files and copy a command with every choice filled in. The project includes `index.js` or `index.ts`, the matching `wa.config` file, a start script, and ignore rules. TypeScript projects include editor types and `tsconfig.json`; Node.js runs these starter files directly. If you selected a plugin that needs credentials, copy `.env.example` to `.env` and fill it in before running `npm start`. ## Connect and handle messages [#connect-and-handle-messages] Scan the terminal QR code using **WhatsApp → Linked devices → Link a device** on your phone. Once ready, send `!ping` from another WhatsApp account. The starter responds `pong` and keeps running. The JavaScript entry point looks like this; the TypeScript choice uses the same flow and imports `wa.config.ts`: ```js title="index.js" import { create } from '@open-wa/wa-automate'; import config from './wa.config.js'; const client = await create(config); console.log('WhatsApp is ready. Send !ping from another account.'); client.onMessage(async message => { if (!message.fromMe && message.body === '!ping') { await client.sendText(message.from, 'pong'); } }); for (const signal of ['SIGINT', 'SIGTERM']) { process.once(signal, () => { void client.stop(signal).catch(console.error); }); } ``` The handler reuses `message.from`, including `@lid` identities. It sends only when someone types `!ping`; startup itself doesn't send a greeting. Add your own handlers here. See [`create()`](/docs/reference/create), [client methods](/docs/reference/client/client), and the [chat ID guide](/docs/guides/chatid-primer). ## Configure browsers and plugins [#configure-browsers-and-plugins] Edit `wa.config.js` or `wa.config.ts`. The entry point passes it to `create()`; it isn't implicitly loaded by the SDK. The generated config also loads `.env` when present. Browser selections and plugins use the same setup options as Easy API. They download only when needed and are cached. SDK mode doesn't start an HTTP server or Neo dashboard. Use [Easy API](/docs/getting-started/easy-api) if you want those surfaces. Chatwoot requires Easy API webhook routes, so the initializer excludes it from SDK mode. Voxtral can run in the SDK using its file settings; see the [Voxtral guide](/docs/guides/voxtral). ## Stop and restart [#stop-and-restart] Press `Ctrl+C` to stop the client and browser. Run `npm start` again from the same directory to reuse a valid saved session. Keep `.env` and session data private. If a send fails with an uncertain result, inspect the receiving chat before retrying it. # Docker (/docs/getting-started/docker) Use Docker to run the Easy API with its browser dependencies in one container. You will need Docker and a writable `./sessions` directory for the WhatsApp profile. Choose a long, random string for your API key and replace `your-secure-key` in the examples with it. These steps start open-wa `5.1.0` and make the API available on your local machine. The commands pin the Docker image digest and the npm package installed at startup. They also pass `--session-id`, `--host`, and `--port` explicitly so the session and listener use the requested values. ## Run the Easy API container [#run-the-easy-api-container] Start with this baseline. It publishes port `8080` on loopback, names the session `sales`, persists its browser profile in `./sessions/sales`, and sets the API key for protected requests: ```bash docker run --name openwa-sales --init \ -p 127.0.0.1:8080:8080 \ -v "$PWD/sessions:/sessions" \ -e W_A_V=5.1.0 \ -e WA_PORT=8080 \ -e WA_HOST=0.0.0.0 \ -e WA_SESSION_ID=sales \ -e WA_USER_DATA_DIR=/sessions/sales \ -e WA_API_KEY="your-secure-key" \ openwa/wa-automate:latest@sha256:d31f5a8f59c4890a302294e9834a25f217a973d80339896ef74cf3d35d903cb3 \ --session-id sales --host 0.0.0.0 --port 8080 ``` Complete authentication in the terminal, then check readiness from the host: ```bash curl http://localhost:8080/health ``` `/health` is public and can include QR and operational details, so the loopback binding is important; `status: "ok"` confirms the HTTP process is live, while `connected: true` and `session.ready: true` confirm WhatsApp readiness. The mounted `./sessions` directory contains authentication state. Keep it private and do not remove it when recreating the container. ## Version pins and image source [#version-pins-and-image-source] Docker Hub currently uses a moving `latest` tag, so the commands use the immutable OCI digest shown in each image reference. The image startup script otherwise installs npm `latest`; `W_A_V=5.1.0` selects the [published npm release](https://www.npmjs.com/package/@open-wa/wa-automate/v/5.1.0) used by these examples. See the [Docker Hub image](https://hub.docker.com/r/openwa/wa-automate/tags) and its [pinned startup script](https://raw.githubusercontent.com/open-wa/wa-automate-docker/5e3cf42b9a5549e85804c31c05cc05f1c58fda47/start.sh). The image declares Node.js `23.8.0`, which is above v5.1.0's `22.21.1` minimum. ## Environment variables and secrets [#environment-variables-and-secrets] The runtime configuration loader consumes `WA_*` names. `WA_API_KEY` configures the Easy API key, `WA_SESSION_ID` names the session, and `WA_USER_DATA_DIR` selects the profile; see [Configuration and CLI](/docs/guides/configuration-and-cli) for supported settings. Use these prefixed names when you configure the runtime. For a local `.env` file, keep it outside version control and load it explicitly with Docker: ```dotenv WA_PORT=8080 WA_HOST=0.0.0.0 WA_SESSION_ID=sales WA_USER_DATA_DIR=/sessions/sales WA_API_KEY=your-secure-key W_A_V=5.1.0 ``` ```bash docker run --name openwa-sales --init \ -p 127.0.0.1:8080:8080 \ -v "$PWD/sessions:/sessions" \ --env-file .env \ openwa/wa-automate:latest@sha256:d31f5a8f59c4890a302294e9834a25f217a973d80339896ef74cf3d35d903cb3 \ --session-id sales --host 0.0.0.0 --port 8080 ``` The env file supplies the same values as the baseline. The command arguments set the session and listener explicitly; Docker forwards requests from loopback to the container. Keep `.env` out of version control and inject these values through your platform's secret store in production. ## Docker Compose [#docker-compose] Compose keeps the port, profile mount, environment, and process init together: ```yaml services: openwa: image: openwa/wa-automate:latest@sha256:d31f5a8f59c4890a302294e9834a25f217a973d80339896ef74cf3d35d903cb3 init: true ports: - "127.0.0.1:8080:8080" volumes: - ./sessions:/sessions environment: WA_PORT: "8080" WA_HOST: "0.0.0.0" WA_SESSION_ID: sales WA_USER_DATA_DIR: /sessions/sales WA_API_KEY: ${WA_API_KEY:?Set WA_API_KEY in .env before starting} W_A_V: 5.1.0 command: ["--session-id", "sales", "--host", "0.0.0.0", "--port", "8080"] healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/health || exit 1"] interval: 30s timeout: 10s retries: 3 ``` Start it with the key supplied from the shell or an ignored `.env` file: ```bash WA_API_KEY="your-secure-key" docker compose up ``` The Compose health check only confirms an HTTP response; it does not confirm WhatsApp authentication. ## `--init` and browser resources [#--init-and-browser-resources] Use `--init` when running the container directly. The browser creates child processes, and an init process reaps them when they exit. Give the container enough memory and CPU for WhatsApp Web, especially when running multiple sessions. These old low-memory Chromium flags break the multi-process/frame model and can produce `Navigating frame was detached` errors. The runtime strips them from `chromiumArgs` by default; setting `allowDangerousBrowserArgs: true` opts into an unsupported configuration. On constrained hosts, keep the runtime's normal browser arguments and add `--shm-size=1gb` to the launch command to give Chrome more shared memory. A single session typically needs **700 MB–1 GB of RAM**. See [browser launch troubleshooting](/docs/operations-and-troubleshooting/error-handling) if Chrome fails to start or exits unexpectedly. ## Verify persistence after a restart [#verify-persistence-after-a-restart] After the first successful authentication: 1. Stop and remove only the container, leaving `./sessions` in place: ```bash docker stop openwa-sales docker rm openwa-sales ``` 2. Run the same baseline command again with the same `WA_SESSION_ID` and `WA_USER_DATA_DIR`. 3. Check `/health` until `connected: true` and `session.ready: true`. The runtime should reuse `/sessions/sales` without a new QR prompt. If a new QR appears, inspect the host mount and the profile path before deleting any session data. ## Use the image as a base for your own code [#use-the-image-as-a-base-for-your-own-code] If you need the browser/runtime dependencies but want your own entrypoint: ```dockerfile FROM openwa/wa-automate:latest@sha256:d31f5a8f59c4890a302294e9834a25f217a973d80339896ef74cf3d35d903cb3 ENTRYPOINT [] # copy your project and run your own command afterwards ``` Keep business logic in your service and call the Easy API or consume events from it. Embedded Node.js code that owns the browser belongs in **[Custom code](/docs/getting-started/custom-code)**. ## Production notes [#production-notes] Keep the API port private, persist the session profile, inject secrets at runtime, and configure `WA_API_KEY` for protected API methods. The API key does not replace WhatsApp authentication. ## v4 archive [#v4-archive] The npm v4 archive remains available for existing v4 applications: ```bash npx @open-wa/wa-automate@4.76.0 ``` Docker Hub does not provide a matching versioned `4.76.0` image tag. Keep v4 configuration and session profiles separate from a v5 deployment. New npm examples on this site target `5.1.0`. # Easy API quick start (/docs/getting-started/easy-api) # Easy API quick start [#easy-api-quick-start] Use Easy API to control WhatsApp from an HTTP client and manage the session in Neo. Use Node.js 22.21.1 or newer and npm. ## Start and pair the session [#start-and-pair-the-session] Create your project, choosing JavaScript or TypeScript, a browser, and optional plugins: ```bash npm init @open-wa@latest my-api -- --mode easy-api cd my-api ``` Use the [visual builder](/new?mode=easy-api) to preview the same choices and files. Easy API discovers the generated `wa.config.js` or `wa.config.ts` file automatically. If you selected integrations, copy `.env.example` to `.env` and fill in their settings. Set an API key and start the server: ```bash title="Terminal" export OPENWA_API_URL='http://127.0.0.1:8080' export WA_API_KEY='REPLACE_WITH_YOUR_API_KEY' npm start ``` On the phone, open **WhatsApp → Linked devices → Link a device** and scan the QR code. Open `/dashboard/` on the printed server address to manage the session. Use `/api-docs/` for the API explorer. The server can answer HTTP requests before WhatsApp is ready. Read `/health` and check `connected` and `session.ready` before sending: ```bash curl --fail-with-body "$OPENWA_API_URL/health" ``` Restart from the same project directory with the same session ID to reuse a valid saved login. Treat session data as private authentication material. Use a different session ID and port for another account. ## Send a message [#send-a-message] Use a chat ID from a conversation already available to the linked account. Starting a new chat requires an applied restricted or premium license. Replace the example destination before sending. The examples use the same URL and key variables as the startup command. Run them in another terminal with those variables set. JavaScript uses Node.js and its built-in `fetch`. For Python, install `requests` with `python -m pip install requests`. cURL JavaScript Python ```bash title="HTTP request" curl --fail-with-body "$OPENWA_API_URL/api/messages/sendText" \ -H "X-API-Key: $WA_API_KEY" \ -H 'Content-Type: application/json' \ --data '{ "to": "447700900123@c.us", "content": "Hello from Easy API", "options": {} }' ``` ```js title="Node.js fetch" const response = await fetch( `${process.env.OPENWA_API_URL}/api/messages/sendText`, { method: 'POST', headers: { 'X-API-Key': process.env.WA_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: '447700900123@c.us', content: 'Hello from Easy API', options: {}, }), }, ); if (!response.ok) throw new Error(await response.text()); const { data: messageId } = await response.json(); console.log(messageId); ``` ```python title="Python · requests" import os import requests response = requests.post( os.environ["OPENWA_API_URL"].rstrip("/") + "/api/messages/sendText", headers={"X-API-Key": os.environ["WA_API_KEY"]}, json={ "to": "447700900123@c.us", "content": "Hello from Easy API", "options": {}, }, timeout=30, ) response.raise_for_status() print(response.json()["data"]) ``` Method routes return `{ "success": true, "data": ... }`. For `sendText`, `data` is the submitted message ID. It does not confirm delivery or reading. An HTTP failure returns an error response. Inspect the conversation before repeating a request whose outcome is unknown. ## Receive events [#receive-events] Open the Server-Sent Events stream before sending messages: ```bash title="Incoming messages and acknowledgements" curl --no-buffer --fail-with-body \ "$OPENWA_API_URL/api/events?topics=message.received,ack.changed" \ -H "X-API-Key: $WA_API_KEY" ``` Each `data:` line contains a JSON envelope with `id`, `event`, `payload`, `ts`, and `sessionId`. Incoming messages are at `payload.message`; ACK snapshots are at `payload.ack`. Close the connection to stop listening. Reconnect after a disconnect; this stream does not replay missed events. For a Node.js application, [SocketClient](/docs/client-and-integrations/socket-client) provides SDK-style methods and listeners over the remote API. For HTTP webhooks, configure the integration plugin in a config file: ```js title="wa.config.js" export default { host: '127.0.0.1', port: 8080, apiKey: process.env.WA_API_KEY, plugins: ['@open-wa/integration-webhook'], pluginConfig: { webhook: { url: 'https://your-app.example/webhooks/open-wa', events: ['message.received'], headers: { 'X-Webhook-Secret': process.env.OPEN_WA_WEBHOOK_SECRET, }, }, }, }; ``` Run `npm start` to load the updated config. See [webhook payloads](/docs/client-and-integrations/webhook-payloads) for event bodies and delivery behavior. ## Use media, events, and calls [#use-media-events-and-calls] * [Media](/docs/guides/media) explains images, videos, voice notes, view-once options, and albums. * [Events and RSVPs](/docs/guides/whatsapp-events) explains native event cards and attendance responses. * [Calls](/docs/guides/calls) explains the licensed audio preview, files, streams, and the Calls dashboard. * [Session refresh](/docs/operations/session-refresh) explains patch and license operations without restarting the API host. For direct HTTP media requests, file paths belong to the Easy API host. Use an accessible URL or complete data URL to supply an attachment from another machine. SDK methods and Web streams cannot be serialized into JSON; use the wire fields listed in `/api-docs/`. ## Remote access [#remote-access] Send `X-API-Key` on protected API requests. Keep the key on your application server and use HTTPS for remote access. `/health` is public and can contain pairing or diagnostic data, so keep the server on a private address and exclude it from a public proxy. Use [Cloudflare Session Proxy](/docs/client-and-integrations/cf-proxy) for remote consumers, [Docker](/docs/getting-started/docker) for a container setup, or [custom code](/docs/getting-started/custom-code) to own the browser directly. # Create a project (/docs/getting-started/initializer) # Create a project [#create-a-project] Use **Node.js 22.21.1 or newer** and npm. Run the interactive wizard: ```bash npm init @open-wa@latest my-bot cd my-bot npm start ``` The wizard asks for **Easy API or SDK**, **JavaScript or TypeScript**, a browser, and optional plugins. Use the [project builder](/new) to make the same choices on this site, preview every generated file, and copy a complete command or a shareable link. Both interfaces use the same options and templates. ## Choose your project [#choose-your-project] **Easy API** runs the HTTP server and Neo dashboard. It discovers `wa.config.js` or `wa.config.ts` automatically. Open [Neo](http://127.0.0.1:8080/dashboard/) after starting the project. **SDK** creates a persistent bot in `index.js` or `index.ts`. It imports your config and passes it to `create()`. Once connected, send `!ping` from another WhatsApp account to receive `pong`; add your own handlers in the entry file. SDK mode runs without an HTTP server or Neo. Choose Easy API for those controls. JavaScript projects use `.js` files. TypeScript projects use `.ts` files and include `tsconfig.json` and editor types. Node.js runs the generated TypeScript directly; you don't need a separate compile step or Bun. ## Browser choices [#browser-choices] | Choice | What happens | | -------------------- | ------------------------------------------------------------------------------------------------ | | Detect at startup | Reuse installed Chrome or a cached browser. Ask what to download if none is available. | | Use installed Chrome | Require a local Chrome installation. Never download a browser. | | Download Chrome | Use managed Chrome for Testing, downloading only when missing. Recommended for calls and media. | | Download Chromium | Use managed Chromium, downloading only when missing. Some audio/video codecs may be unavailable. | The terminal wizard shows the Chrome installation it detects. The website can't inspect installed applications, so its command performs that check locally. Browser downloads happen when you start the project and are cached for reuse. Choose an explicit download option for an unattended first start. ## Plugins and credentials [#plugins-and-credentials] Select Voxtral, Webhooks, or Chatwoot. Chatwoot needs Easy API webhook routes and is unavailable in SDK mode. Selecting Voxtral also asks whether to handle voice notes, calls, or both. Calls need a calling-enabled open-wa license. The initializer writes plugin settings into the config and puts the required environment variable names in `.env.example`. Copy that file to `.env` and fill in your values before starting the integrations. The generated config loads `.env`; existing environment variables take precedence. Keys stay on your computer and aren't included in the builder's command or share link. Only selected plugins are downloaded, on demand at startup, then reused from the project's plugin cache. There is no separate plugin installation command. ## Repeatable commands [#repeatable-commands] Pass flags to skip their questions. Partial flags leave the remaining questions interactive. Add `--yes` to use defaults for any missing choices: ```bash npm init @open-wa@latest my-api -- --mode easy-api --language js --browser chrome --plugins none --yes ``` For a TypeScript SDK voice assistant: ```bash npm init @open-wa@latest voice-bot -- --mode sdk --language ts --browser chrome --plugins voxtral --voxtral-mode both --yes ``` | Flag | Values | | ---------------- | ------------------------------------------------------------ | | `--mode` | `easy-api`, `sdk` | | `--language` | `js`, `ts` | | `--browser` | `auto`, `system-chrome`, `chrome`, `chromium` | | `--plugins` | `voxtral`, `webhook`, `chatwoot`, comma-separated; or `none` | | `--voxtral-mode` | `voice-notes`, `calls`, `both` | | `--no-install` | Write the files and let you run `npm install` later. | | `--yes` | Use defaults for unanswered choices. | | `--help` | Show usage without creating files. | Defaults are Easy API, JavaScript, browser detection, no plugins, voice-note mode when Voxtral is selected, and dependency installation enabled. Use `.` as the project directory to create files in the current folder. ## Generated files [#generated-files] Every project includes `package.json`, `wa.config.js` or `wa.config.ts`, `.gitignore`, and `README.md`. SDK projects also include their `index.js` or `index.ts` entry point. TypeScript adds `tsconfig.json`. Projects with API keys or integration settings include `.env.example`. The initializer checks for existing destination files before writing and refuses to overwrite them. If dependency installation fails, keep the generated files, resolve the npm error, and run `npm install` from the project directory. The existing `npx @open-wa/wa-automate init my-bot` entry point opens the same wizard and supports the same flags. ## Link WhatsApp [#link-whatsapp] Run `npm start`, then open **WhatsApp → Linked devices → Link a device** on your phone and scan the QR code. Keep the project running to handle messages or calls. Stop with `Ctrl+C`; restart from the same project directory to reuse a valid saved login. Treat session storage and `.env` as private credentials. Next: [Easy API quick start](/docs/getting-started/easy-api), [SDK quick start](/docs/getting-started/custom-code), or the [Voxtral guide](/docs/guides/voxtral). ## Text and voice assistants [#text-and-voice-assistants] Select **Jev chat assistant** to answer text messages, and optionally **Voxtral** for voice notes and calls. Both choices are available in the terminal wizard and [project builder](/new?plugins=jev,voxtral). Fill in `MISTRAL_API_KEY` and `JEV_API_KEY` once in the generated `.env`; both plugins share conversation context and chat actions. See [Jev](/docs/guides/jev) and [Voxtral](/docs/guides/voxtral). # Link-code login (/docs/getting-started/link-code) Link-code login is an optional alternative to scanning a QR code. QR pairing remains the default login method. Both methods link a browser session with the WhatsApp account on your phone. This example uses npm release `5.1.0`. Embedded applications can request a code through `createClient` and receive it from the runtime event emitter. The `5.1.0` Easy API CLI does not forward `linkCode` or print the generated event. Use that version's QR flow instead. The `launch.auth.linkCode.generated` event is sensitive. The example prints it only in the local terminal that started the application. Do not forward it to HTTP routes, shared logs, chat, or issue reports. The plugin event gateway blocks sensitive events. ## 1. Create the project [#1-create-the-project] Install the matching runtime and Puppeteer driver packages in a new Node.js project: ```bash mkdir open-wa-link-code cd open-wa-link-code npm init -y npm pkg set type=module npm install @open-wa/wa-automate@5.1.0 @open-wa/driver-puppeteer@5.1.0 ``` Save the following as `index.mjs`. Replace the example number with the full phone number, including country code, using digits only. ```js import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; const runtime = await createClient({ sessionId: 'link-code-demo', driver: new PuppeteerDriver(), headless: false, linkCode: '447123456789', }); runtime.events.on('launch.auth.linkCode.generated', (event) => { const code = event.details?.linkCode; if (code) console.log('Enter this code on your phone:', code); }); const client = new Client({ client: runtime, transport: runtime.getTransport(), }); const shutdown = async (signal) => { await client.stop(signal); }; process.once('SIGINT', () => void shutdown('SIGINT')); process.once('SIGTERM', () => void shutdown('SIGTERM')); await client.start(); const readiness = runtime.getReadiness(); console.log('WhatsApp session ready:', readiness.ready); ``` The event listener is attached before `client.start()` because startup emits the generated code while authenticating. Run the application and keep its terminal open: ```bash node index.mjs ``` ## 2. Enter the code on your phone [#2-enter-the-code-on-your-phone] On the phone that owns the number in `linkCode`: 1. Open WhatsApp. 2. Open **Settings** on iPhone, or **Menu** then **Linked devices** on Android. 3. Tap **Link a device**. 4. Choose **Link with phone number instead**. The wording can vary by platform version. 5. Enter the code printed in the computer's local terminal and confirm the link. Use digits only in `linkCode`. Do not include a plus sign, spaces, brackets, or dashes. For example, use `447123456789`, not `+44 7123 456789`. A chat ID such as `447123456789@c.us` belongs in messaging calls after the session connects, not in `linkCode`. ## 3. Confirm readiness [#3-confirm-readiness] The program prints `WhatsApp session ready: true` after startup completes successfully. In code, `runtime.getReadiness()` exposes `ready`, `pending`, and `blockers`; proceed with messaging only when `ready` is true. Code generation alone does not mean that the phone completed pairing. Link codes expire. If the phone rejects the code or startup times out, stop the current process and start it again to request a fresh code. If the loaded WhatsApp Web build does not support link-code login, the runtime can fall back to QR authentication in the browser window. For a complete embedded lifecycle and messaging example, see **[Custom code](/docs/getting-started/custom-code)**. For the Easy API CLI, use the **[Quick start](/docs/getting-started/quickstart)** QR flow. # Migrate from v4 to v5 (/docs/getting-started/migrating-to-v5) # Migrate from v4 to v5 [#migrate-from-v4-to-v5] This guide uses Bun and TypeScript to move an existing v4 application to v5. If you are starting a new application, use the [project initializer](/docs/getting-started/initializer) instead. **QR codes are still the default way to link your WhatsApp account.** Scan the terminal QR code from **Linked Devices** on your phone. Link-code login is an optional alternative. The saved login changes. Current v5 saves an encrypted session file instead of keeping a full browser profile. Keep that file to reuse your login on later runs. When moving from v4, use a separate folder and scan once to create the new v5 session file. v4 and earlier are legacy releases. open-wa runs against WhatsApp Web, which changes independently of your installed package. An older release can stop authenticating, sending messages, or receiving events even when you have changed nothing in your application. Pinning a package keeps your application on that package's code; it cannot pin WhatsApp Web or guarantee that the integration will keep working. This applies to older v5 releases too, so plan to keep open-wa updated. The migration involves more than changing a version number. Some familiar methods remain, some have different inputs or results, and some v4 features have no supported replacement yet. Start with the changes below, then check the [known gaps](#known-gaps) before moving a workflow that depends on them. > **Release status:** The CLI startup fix and restored client methods and > listeners described here are changes for the next package release. They are > not included in 5.4.0. That release can fail during startup before printing a > QR code. Follow these examples with a release that includes the fixes. > `@latest` selects the published npm release, not the code on `master`. ## What needs to change? [#what-needs-to-change] This table describes an application that imports `@open-wa/wa-automate` and owns its WhatsApp browser session. Remote consumers also need the [Easy API and SocketClient changes](#migrate-easy-api-and-socketclient). | Your v4 code | What to do in v5 | | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `import { create } from '@open-wa/wa-automate'` | Keep it! `create()` returns a ready messaging client. You do not need to replace it with `createClient()`. | | `client.onMessage(callback)` and `client.onAnyMessage(callback)` | Keep the callback pattern. Update any code that uses the registration result or removes listeners. | | `client.sendText(to, text)` | Keep the arguments. Handle a thrown error instead of checking for a `false` result. | | `client.kill()` | Replace it with `client.stop()` to close the browser. | | `ConfigObject` or `AdvancedConfig` | Use `CreateOptions` for the options passed to `create()`. Review the configuration table below. | | `ev.on(...)` from a package-level import | Keep it. Import the same shared helper in each file. | | `onAck((message) => ...)` | Keep the message callback, including `message.id`, `message.ack`, and the message details. | | `onStateChanged` checks for `CONNECTED` | Read the core lifecycle with `client.getState()` and check for `READY`. | | `getGroupMembers()` and `getGroupMembersId()` | Keep both: the first returns contacts, the second returns IDs. | | QR code pairing | Keep it. Scan the terminal QR code from **Linked Devices** on your phone. | | A saved v4 `.data.json` file or base64 session string | Keep a backup. Use a separate v5 session folder and scan a QR code to save the new login file. | ## 1. Update the dependency [#1-update-the-dependency] Stop the old bot and keep a backup of its configuration and session storage. Use a separate session-data directory for v5 and scan a QR code to link the account again. Running both bots against the same conversations with reply handlers enabled can produce duplicate replies. Once the release containing these fixes is available, run this in your existing application directory with [Bun installed](https://bun.sh/docs/installation): ```bash bun add @open-wa/wa-automate@latest ``` You do not need to run the initializer over an existing application. If you already depend directly on other `@open-wa` packages, update those dependencies to compatible v5 releases too; do not combine a v4 remote client with a v5 server. See the [package guide](/docs/concepts/packages) for what each package provides. Keep imports on public package exports. Replace any private import such as `@open-wa/wa-automate/dist/...` with its public equivalent. The [client reference](/docs/reference/client) lists the public types and methods. ## 2. Keep create and update shutdown [#2-keep-create-and-update-shutdown] A typical v4 bot looks like this: ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'support', multiDevice: true }); await client.onMessage(async (message) => { if (message.body === '!ping') { await client.sendText(message.from, 'Pong!'); } }); process.once('SIGINT', () => void client.kill()); ``` In v5, the same bot can use this complete `index.ts`: ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'support', sessionDataPath: './v5-sessions', }); client.onMessage(async (message) => { if (message.fromMe || message.body !== '!ping') return; try { await client.sendText(message.from, 'Pong!'); } catch (error) { console.error('Reply failed. Check the chat before retrying:', error); } }); let stopping = false; const shutdown = async () => { if (stopping) return; stopping = true; await client.stop(); }; process.once('SIGINT', () => void shutdown()); process.once('SIGTERM', () => void shutdown()); console.log('Ready. Send !ping to this account from another chat.'); ``` Run it with: ```bash bun run index.ts ``` `./v5-sessions` stores the new session file, relative to where you run the application. The first run prints a QR code. On iPhone, open WhatsApp **Settings → Linked Devices**. On Android, open the WhatsApp menu and select **Linked Devices**. Tap **Link a Device**, then scan the terminal QR code. After the `Ready` message, send `!ping` from another chat and look for `Pong!`. Keep `./v5-sessions/support.data.json` after pairing. Restart the application with the same session ID and folder to restore the saved login. You do not need to scan a QR code on every restart. [`create()`](/docs/reference/create) chooses Puppeteer, provisions its matched browser if needed, handles terminal pairing, and resolves when the session is ready. You do not need an additional `client.start()` call. Browser provisioning does not install missing operating-system libraries. [`stop()`](/docs/reference/create#stop) closes the browser and leaves its saved login available for the next run. It does not log the account out of WhatsApp. ### When createClient is useful [#when-createclient-is-useful] Use [`createClient()`](/docs/reference/core) when you need to supply a driver, control startup explicitly, or load plugins through the core runtime. It returns an unstarted runtime, not the messaging `Client`. Register startup handlers first, wrap it in `new Client({ client: runtime, transport: runtime.getTransport() })`, then call `await client.start()`. That lower-level API is optional. Keep `create()` for a straightforward bot. You can subscribe to the shared `ev` before calling it to observe startup; see [global events](#keep-the-global-ev-import) below. ## 3. Review configuration and saved sessions [#3-review-configuration-and-saved-sessions] Use [`CreateOptions`](/docs/reference/create) with `create()`. The low-level [`CreateClientOptions`](/docs/reference/core#createclientoptions) type uses some different names and units. | Setting | With `create()` | With low-level `createClient()` | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `sessionId`, `headless`, `executablePath` | Keep these names. | Keep these names; also supply a `driver`. | | `qrTimeout`, `authTimeout`, `oorTimeout` | Values are in seconds. `create()` defaults `qrTimeout` to `0`, with no QR scan deadline. | Use `qrTimeoutMs`, `authTimeoutMs`, `oorTimeoutMs`, in milliseconds. | | `chromiumArgs` | Keep this name. | Use `browserArgs`. | | `linkCode`, `qrMax`, `licenseKey` | Keep these names. | Keep these names. | | `eventMode` | Defaults to `true`, forwarding supported listener events to `ev`. | Same behavior. | | `multiDevice` | Remove it. It no longer selects an authentication mode. | No such option. | | `sessionDataPath` | Directory or explicit `.data.json` file for compact session storage. | Same behavior. | | `sessionData` | An encrypted compact session exported by current v5, as an object, JSON, or base64 JSON. | Same behavior. | | `userDataDir`, `ephemeral` | Remove assumptions that these control saved login. The browser profile is disposable. | Same behavior. | | `skipSessionSave` | Disables the local session file and requires a durable remote store. | Same behavior. | | `sessionDataEncryptionKey`, `sessionDataEncryptionKeyFile` | Optional custom key or mounted key file; choose one. | Same behavior. | | `onError`, `pQueueDefault` | Handle errors explicitly and configure listeners individually. | No direct equivalent options. | | `messagePreprocessor`, `preprocFilter` | Use host `hooks.beforeEvent` for pre-dispatch filtering and `hooks.chatFilter.allow` to restrict plugin chats. | `hooks` | For example, one minute is `authTimeout: 60` with `create()` and `authTimeoutMs: 60_000` with `createClient()`. Copying the number `60` into the millisecond option would allow only 60 milliseconds. ### Keep the saved login for later runs [#keep-the-saved-login-for-later-runs] After pairing, v5 saves your login in `.data.json`, in the directory selected by `sessionDataPath`. The example above saves `support.data.json` in `./v5-sessions`. The documentation calls this encrypted file a compact session. Restart with the same session ID, storage path, and custom encryption key, if you supplied one. The saved login lets v5 reconnect without another QR scan. Without a custom key, the runtime uses its default encryption configuration. V5 starts a fresh browser profile and restores the saved login from the file. You do not need to keep the browser profile directory. Keep the session file in private persistent storage because it contains login credentials. [`stop()`](/docs/reference/create#stop) closes the browser without unlinking the account or deleting its saved login. An old v4 profile or token file cannot restore this v5 login. Keep the old storage as a backup and pair once in the separate v5 directory. If the new file is missing or unusable, or you unlink the account, you must pair again. ### If no QR code appears [#if-no-qr-code-appears] If v5 restores a saved login, no QR scan is needed. If startup reports `Bootstrap failed`, the application has failed before it can finish linking. QR pairing has not been removed. An unusable session file can cause `PORTABLE_SESSION_PROVISION_FAILED` before the QR code appears. Use the separate v5 folder shown above to keep old files out of the new login path. Published 5.4.0 also has the startup issue described in the release notice at the start of this guide. ### Pass configuration to an embedded application [#pass-configuration-to-an-embedded-application] The CLI reads `wa.config.ts` and `WA_*` environment variables. Embedded `create()` does not load general configuration from those sources; pass the values your application needs. Session encryption keys have their own secret-loading path and can also come from `OPENWA_SESSION_DATA_ENCRYPTION_KEY` or `WA_SESSION_DATA_ENCRYPTION_KEY`, or their `_FILE` variants. ```ts import { create, type CreateOptions } from '@open-wa/wa-automate'; const options = { sessionId: 'support', sessionDataPath: './v5-sessions', licenseKey: process.env.WA_LICENSE_KEY, } satisfies CreateOptions; const client = await create(options); ``` See the [session and login options](/docs/reference/create#session-and-login-options) and [configuration guide](/docs/guides/configuration-and-cli) for deployment settings. Keep secret values out of copied examples. ## 4. Update listeners and event handlers [#4-update-listeners-and-event-handlers] [`onMessage` and `onAnyMessage`](/docs/guides/messages#receive-messages) still pass a message to your callback. Keep a `fromMe` guard in reply handlers so they cannot answer their own messages. Registration now returns a listener handle immediately. Remove that particular subscription with `handle.off()`, instead of relying on the old `Promise` result or `removeListener(SimpleListener.Message)`: ```ts const subscription = client.onMessage((message) => { console.log('Received message:', message.id); }); // When this part of your application no longer needs the subscription: subscription.off(); ``` Adding `await` to registration is unnecessary. Pass [queue options](/docs/guides/messages#listener-queues) to the individual listener rather than expecting the v4 `pQueueDefault` option to configure it. ### Keep the full acknowledgement message [#keep-the-full-acknowledgement-message] Your v4 acknowledgement handler can keep reading the message: ```ts const subscription = client.onAck((message) => { console.log(message.id, message.ack, message.body); if (message.sender) console.log('Sender:', message.sender.id); }); ``` The callback receives the available message snapshot when its acknowledgement changes. open-wa preserves the body, sender, chat, and extra WhatsApp fields that are present; it does not fetch the message later or reduce it to an ID and status. Some details can be absent while WhatsApp is loading a message. Use `subscription.off()` when you no longer need the handler. If you already adapted to early v5's `{ messageId, ack }` callback, use `message.id` instead of `messageId`. See [acknowledgements](/docs/guides/messages#acknowledgements) for the delivery states. ### Connection states describe the core lifecycle [#connection-states-describe-the-core-lifecycle] The v5 core lifecycle uses `STARTING`, `AUTHENTICATING`, `READY`, `DISCONNECTED`, and `STOPPED`. Use `client.getState()` to read that lifecycle rather than treating a raw WhatsApp Web `CONNECTED` notification as proof of readiness. The runtime bridge can also forward browser state notifications. Do not carry over calls to `forceRefocus()` without a supported equivalent. ```ts client.onStateChanged(() => { const state = client.getState(); if (state === 'READY') console.log('WhatsApp is ready'); if (state === 'DISCONNECTED') console.warn('WhatsApp disconnected'); }); ``` Because `create()` resolves after startup, a listener added afterward observes future state changes. Use `runtime.events` before starting when you need the initial transition. [`onLogout`](/docs/guides/session-events) also changed: its callback receives `{ reason?, timestamp }`, rather than v4's optional boolean. ### Keep the global ev import [#keep-the-global-ev-import] `ev` is shared across imports in the same process. Register listeners before `create()` when you need startup events. You can also use it for your own application events between files. Put the listener in `events.ts`: ```ts import { ev } from '@open-wa/wa-automate'; ev.on('client.ready', (_event, sessionId) => { console.log(`Session ${sessionId} is ready`); }); ev.on('onAck.**', ({ data: message, sessionId }) => { console.log(sessionId, message.id, message.ack); }); ev.on('app.job.finished', ({ jobId }) => { console.log('Finished:', jobId); }); ``` Import it before startup in `index.ts`: ```ts import './events'; import { create, ev } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'support', sessionDataPath: './v5-sessions' }); ev.emit('app.job.finished', { jobId: 'daily-summary' }); ``` `onAck.**` matches `onAck..`. Supported listener aliases carry the v4 envelope `{ ts, sessionId, id, event, data }`. Lifecycle aliases such as `qr.**`, `linkCode.**`, and `sessionData.**` call the listener with `(data, sessionId, namespace)`. `*` matches one name segment and `**` matches zero or more, including patterns such as `**.support`. `sessionData` now contains the encrypted compact session object, not v4's obsolete token fields. QR values are pairing strings for your private UI. Do not send QR values, session data, or link codes to shared logs. `eventMode: false` disables automatic SimpleListener aliases, while lifecycle and custom events remain available. Keep `client.events` when you want one client's native event stream. Use `ev.off(name, callback)` to remove one global subscription; stopping a session removes its forwarding connection without removing another module's global listeners. See [session events](/docs/guides/session-events) for native event names and optional Effect-scoped subscriptions. ## 5. Review the message and group data you read [#5-review-the-message-and-group-data-you-read] You do not need to rename every message field. The v5 message contract retains `id`, `body`, `type`, `from`, `to`, `chatId`, `fromMe`, `isGroupMsg`, `sender`, `chat`, `timestamp`, and `t`. The important changes are assumptions about optional fields and the object passed to each kind of listener. | What your handler needs | What to use in v5 | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Text of a text message | `message.body` when `message.type === 'chat'`. The old convenience field `text` is not a guaranteed field in the v5 schema. | | A media caption | `message.caption ?? ''`; the caption is optional. | | The conversation to reply to | Reuse `message.chatId` or the incoming `message.from` unchanged. Do not reconstruct it from a phone number. | | The person speaking in a group | Check `message.author`, `message.senderId`, or `message.sender.id`; do not confuse them with the group's chat ID. | | Contact labels or display names | Treat `sender.name`, `sender.pushname`, and label arrays as optional. | | Group metadata | `message.chat.groupMetadata` can be absent or `null`; check it before reading `subject` or `participants`. | | Contact details on a chat | `chat.contact` can be `null`. | The [message guide](/docs/guides/messages#message-object-shape) and [Chat ID guide](/docs/guides/chatid-primer) cover these fields. Preserve IDs with an `@lid` suffix when WhatsApp provides them. Message, contact, chat, and group IDs have distinct TypeScript types; use values returned by the corresponding API, and validate externally supplied values against the relevant [identifier contract](/docs/guides/chatid-primer), rather than casting every string to `any`. ### Keep the two group-member methods [#keep-the-two-group-member-methods] The two methods keep their v4 meanings: ```ts const contacts = await client.getGroupMembers(groupId); for (const contact of contacts) { console.log(contact.id, contact.name ?? contact.pushname ?? 'Unnamed contact'); } const ids = await client.getGroupMembersId(groupId); ``` Here `groupId` is a group ID returned by a group lookup, creation, or join operation. `getGroupMembers()` returns `Contact[]`; `getGroupMembersId()` returns `ContactId[]`. There is no need to map IDs through `getContact()` yourself. Use the ID-only method for membership checks that do not need contact details. If WhatsApp cannot resolve a member's contact, the contact lookup rejects rather than silently omitting that member. See the [group reference](/docs/reference/client/groups). If you adapted to early v5's ID-returning `getGroupMembers()`, change that call to `getGroupMembersId()` to keep receiving IDs. The v5 embedded `addParticipant`, `promoteParticipant`, and `demoteParticipant` methods take one contact ID per call. Replace a v4 array argument with an explicit loop and handle each result. `removeParticipant` also takes one ID. The connected account still needs the relevant group permissions. ## 6. Replace renamed methods and handle changed results [#6-replace-renamed-methods-and-handle-changed-results] These are embedded-client changes. The [method reference](/docs/reference/client/client) separates embedded, SocketClient, and Easy API examples because their contracts can differ. | v4 usage | v5 embedded equivalent or action | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `getSessionId()` | Read `client.sessionId`. | | `kill(reason)` | Call [`stop(reason)`](/docs/reference/create#stop); it resolves without the old boolean result. | | `logout(preserveSessionData)` | No equivalent embedded logout method is exposed. `stop()` closes the browser while preserving login; use WhatsApp's Linked Devices screen when you intend to unlink the session. | | `getConnectionState()` | Use synchronous `getState()` for the v5 lifecycle state. The legacy-named method is unsupported. | | `getAllMessagesInChat(chatId, includeMe, includeNotifications)` | Use [`getAllMessages(chatId, includeMe, includeNotifications)`](/docs/reference/client/messages#getallmessages). | | `getAllChats(true)` | Call `getAllChats()` and filter the result, for example by `(chat.unreadCount ?? 0) > 0` for an unread-chat view. The old boolean argument is not accepted. | | `archiveChat(chatId, true)` / `archiveChat(chatId, false)` | Call [`archiveChat(chatId)` / `unarchiveChat(chatId)`](/docs/reference/client/chats#archivechat). | | `getProfilePicFromServer(contactId)` | Use [`getProfilePic(contactId)`](/docs/reference/client/contacts#getprofilepicture) on the embedded client. | | `sendLocation(to, '51.5', '-0.1', label)` | Pass numeric coordinates: [`sendLocation(to, 51.5, -0.1, label)`](/docs/reference/client/messages#sendlocation). | | `sendImage(to, localPathOrUrl, filename, caption)` | Supply image data as base64 or a data URL; the embedded method does not load a local path or fetch a URL for you. See [media](/docs/guides/media). | | `sendImageAsSticker(...)` | Review [`sendSticker(...)`](/docs/guides/media#stickers), which accepts prepared sticker data. It is not the old general image-conversion helper. | | Package-level `decryptMedia(message)` | Use [`client.decryptMedia(message)`](/docs/guides/media) for a data URL, or `client.downloadMedia(message, path)` to save the bytes. The return is not a `Buffer`. | | `sendText(...)` returns `false` on failure | Catch [`SendTextError`](/docs/guides/messages#send-messages). A successful call returns a validated message ID. | ### A send failure may have an uncertain outcome [#a-send-failure-may-have-an-uncertain-outcome] Replace code such as `if (!await client.sendText(...)) retry()` with explicit error handling. A browser connection can fail after a message was submitted, so a blind retry can send it twice. ```ts import { SendTextError } from '@open-wa/wa-automate'; // Inside your existing message handler: try { const messageId = await client.sendText(message.from, 'Request received!'); console.log('Submitted:', messageId); } catch (error) { if (error instanceof SendTextError && error.outcome === 'unknown') { console.error('The send could not be confirmed. Check the chat before retrying.'); } else { console.error('Send failed:', error); } } ``` A message ID confirms submission, not delivery or reading. Use acknowledgements when your workflow needs those later states. The error change above applies to `sendText`; do not assume that every other send method has the same return contract. ## Migrate Easy API and SocketClient [#migrate-easy-api-and-socketclient] An embedded bot does not need to become an HTTP client to migrate. This section is for applications that already communicate with a separate open-wa process. ### Start the v5 server [#start-the-v5-server] Start in a new folder so v5 does not read an old session file. Use an explicit session name and private bind address: ```bash mkdir open-wa-v5 cd open-wa-v5 bunx --bun @open-wa/wa-automate@latest --session-id migration --host 127.0.0.1 --port 8080 ``` Scan the terminal QR code from **Linked Devices** on your phone. Keep the new session file and use this same folder for later starts. Link-code login is optional. The release notice above also applies to this CLI example. For an existing deployment, review its `wa.config.ts`, session-data path, API key, and [CLI flags](/docs/guides/configuration-and-cli#full-cli-reference) before switching traffic. Finish the [pairing and readiness steps](/docs/getting-started/quickstart) first. `/health` must report `connected: true` and `session.ready: true` before messaging; an HTTP listener alone does not mean WhatsApp is ready. ### Update HTTP requests and response handling [#update-http-requests-and-response-handling] Use the HTTP verb and route in the running server's [API explorer](http://localhost:8080/api-docs/). Do not assume every operation is a `POST` to a bare `/` path. For example, sending text uses `POST /api/messages/sendText` with named fields, and the result is inside an envelope: ```ts // send.ts: run with bun run send.ts after replacing the recipient. const recipient = 'REPLACE_WITH_AN_EXISTING_CHAT_ID'; if (recipient.startsWith('REPLACE_')) throw new Error('Set recipient first'); const response = await fetch('http://127.0.0.1:8080/api/messages/sendText', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: recipient, content: 'Hello from v5!' }), }); const result = await response.json(); if (!response.ok || !result.success) { throw new Error(`Send failed: ${JSON.stringify(result)}`); } console.log('Submitted:', result.data); ``` Use a chat whose recipient expects the message. If the server has an API key, include it in the `X-API-Key` request header. The [sendText reference](/docs/reference/client/messages#sendtext) includes the remote request contract. Namespaced HTTP routes do not mean an embedded client must change `client.sendText(...)` to `client.messages.sendText(...)`. ### Replace the old remote transport [#replace-the-old-remote-transport] v5 [`SocketClient`](/docs/client-and-integrations/socket-client) uses HTTP for commands and Server-Sent Events for events. It is not a Socket.IO connection. Update any reverse proxy rules or custom Socket.IO code accordingly. ```bash bun add @open-wa/socket-client@latest ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect( 'http://127.0.0.1:8080', process.env.WA_API_KEY, ); client.socket.on('message.received', ({ message }) => { console.log('Received message:', message.id); }); process.once('SIGINT', () => client.close()); process.once('SIGTERM', () => client.close()); ``` The remote event payload wraps the message under `message`; the embedded `client.onMessage()` callback receives the message directly. Keep server and client releases aligned. Closing a remote consumer ends its connection; it does not stop the server's WhatsApp browser. For webhook integrations, review the v5 [envelope](/docs/client-and-integrations/webhook-payloads#envelope): the event-specific value is under `payload`, and an incoming message is under `payload.message`, instead of the v4 `data` field. A configured `--webhook` flag alone does not establish v5 delivery. The v5 CLI also does not load the shared plugin configuration; use the [core plugin-loading path](/docs/guides/configuration-and-cli#plugins-field-docs) when migrating a webhook integration. ## Keep the file, message, and group methods [#keep-the-file-message-and-group-methods] The client binds these methods through the schema system to their existing private patch implementations. Keep the flat client names below. Their absence from the bundled `wapi.js` does not mean they were removed from the patches. The restored bindings are part of the next package release described above. | Method | v5 client behavior | | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | `sendFile(to, file, filename, caption)` | Sends through the existing private `WAPI.sendFile` implementation. The file schema accepts a file path, URL, data URL, base64, or bytes. | | `sendFileFromUrl(to, url, filename, caption, headers)` | Downloads the file on the host and delegates to `sendFile`. | | `editMessage(messageId, newContent)` | Calls the existing private editing method. | | `react(messageId, emoji)` | Calls the existing private reaction method. | | `getGroupInfo(groupId)` | Returns the patch's group title and description. Check for a returned error string before reading the result. | | `setGroupTitle(groupId, title)` | Calls the existing private group-title method. | | `setGroupDescription(groupId, description)` | Calls the existing private group-description method. | | `onParticipantsChanged(groupId, callback)` | Subscribes to participant changes for one group. Use `handle.off()` to remove the subscription. | | `onGlobalParticipantsChanged(callback)` | Subscribes to participant changes across the session's groups. | Participant callbacks retain the patch's `who`, `chat`, `action`, and `by` fields. The schema also supplies `groupId` and `participantIds`. The same change is available as `group.participants.changed.global` on `client.events` and through the shared `ev` listener aliases. ## Known gaps [#known-gaps] Stable v5 does not mean that every v4 feature has parity. These limitations describe the v5 embedded client and its shipped browser runtime. A method or event appearing in a generated schema is not proof that the running integration implements it. | Feature your application may use | v5 limitation | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Other group methods | `getGroupAdmins` is not an embedded `Client` method. | | Added-to-group notifications | `onAddedToGroup` is not an embedded convenience method. The optional `client.events` event `group.addedToGroup` supplies a `groupId`, not the full v4 chat object. | | Message deletion notifications | `onMessageDeleted` throws an unsupported-listener error; there is no shipped deletion bridge. | | Other v4 listeners | Do not assume call, battery, poll, reaction, label, or commerce listeners work because their names remain in an event schema. They are not among the embedded client's supported listeners. | | Chat settings and session diagnostics | Embedded `pinChat`, `unpinChat`, `muteChat`, `unmuteChat`, `getBlockedContacts`, `getCommonGroups`, `getFeatures`, and `getProcessStats` are unsupported. | | v4 media and interactive helpers | Helpers such as `sendPtt`, `sendAudio`, `sendAdvancedButtons`, and `sendListMessage` are not drop-in embedded methods. Review the [interactive API](/docs/guides/interactive-messages) and its license requirements for a supported interactive workflow. | | Config-driven plugins | Neither the ready `create()` wrapper nor the v5 CLI forwards shared `plugins` / `pluginConfig` into core startup. Use the [core plugin options](/docs/guides/configuration-and-cli#plugins-field-docs) when you need to load plugins. | | Old CLI integrations | `--webhook`, `--ready-webhook`, `--tunnel`, and the legacy Chatwoot, Twilio, and BotPress flags do not restore their v4 behavior just because the CLI accepts or warns about them. | If a required workflow hits one of these gaps, identify that dependency before switching it. A return to v4 is not a compatibility guarantee: the same WhatsApp Web changes can affect that old release. Report the specific missing operation, your installed version, and whether you use the embedded client or Easy API in the [issue tracker](https://github.com/open-wa/wa-automate-nodejs/issues). Exclude session files, QR codes, API keys, and private message content. ## Before switching your application over [#before-switching-your-application-over] Use your application's actual workflows to finish the migration: 1. **Start and restart the session.** Pair v5, stop it, and restart with the same session ID, compact session file, and encryption key if configured. Confirm that WhatsApp restores the session. 2. **Receive a message and send a reply.** Confirm the reply in the receiving chat. Check the identifiers and optional fields your handler depends on. 3. **Exercise the features you use.** Include group operations, media, acknowledgements, remote requests, and integrations where applicable. A successful text reply does not establish those features' availability. 4. **Move traffic once the replacements work.** Stop the old reply handlers, switch to the v5 deployment, and retain the old configuration and profile backup for investigation. Keep the new compact session storage separate. Use [release status](/docs/releases/v5-alpha) and the API reference for the version you install to follow further changes. # Quick start (/docs/getting-started/quickstart) # Quick start [#quick-start] ## Before you start [#before-you-start] Use **Node.js 22.21.1 or newer**, npm, and a phone running WhatsApp. ## 1. Create your project [#1-create-your-project] ```bash npm init @open-wa@latest my-bot cd my-bot ``` Choose **Easy API** to use HTTP requests and the Neo dashboard, or **SDK** to write your own bot. The wizard also lets you choose JavaScript or TypeScript, use installed Chrome or download a browser, and select plugins. Prefer a visual setup? Use the [project builder](/new) to choose your options, preview the files, and copy a complete setup command. See the [initializer guide](/docs/getting-started/initializer) for all flags. If you selected integrations, copy `.env.example` to `.env` and fill in their settings. Selected plugins and any needed browser download are installed at startup and cached for reuse. ## 2. Start and link WhatsApp [#2-start-and-link-whatsapp] ```bash npm start ``` On your phone, open **WhatsApp → Linked devices → Link a device** and scan the QR code shown in the terminal. Keep the terminal open; stop the project with `Ctrl+C`. Restart from the same project directory to reuse a valid saved login. ## 3. Use your project [#3-use-your-project] ### Easy API [#easy-api] Open [Neo](http://127.0.0.1:8080/dashboard/) to manage the session and [the API explorer](http://127.0.0.1:8080/api-docs/) to browse available requests. The HTTP process can start before WhatsApp connects. Check readiness before sending: ```bash curl http://127.0.0.1:8080/health ``` Look for `connected: true` and `session.ready: true`. If either is false, finish pairing and inspect the reported pending steps before sending. Use an existing conversation whose recipient expects your message. Replace the example chat ID, then run this in another terminal: ```bash curl --fail-with-body http://127.0.0.1:8080/api/messages/sendText \ -H 'Content-Type: application/json' \ --data '{"to":"447700900123@c.us","content":"Hello from open-wa","options":{}}' ``` If you set `WA_API_KEY`, also send an `X-API-Key` header with that value. A successful response returns the submitted message ID; check the receiving chat for delivery. In PowerShell, use `curl.exe` for these curl commands. Continue with the [Easy API guide](/docs/getting-started/easy-api) for HTTP authentication, events, and media. ### SDK [#sdk] The generated `index.js` or `index.ts` connects and keeps the bot running. Send `!ping` to the linked account from another account; it replies `pong`. Edit that file to add your own handlers. The generated config is imported and passed to `create()` explicitly. Continue with the [SDK guide](/docs/getting-started/custom-code). ### Voxtral [#voxtral] If you selected Voxtral, set `MISTRAL_API_KEY` in `.env` before starting. It answers voice notes, calls, or both according to your selection. Set `JEV_API_KEY` to enable chat actions such as booking cards and buttons. Calls also need `OPENWA_LICENSE_KEY` with calling enabled. In Easy API, use **Plugins → Voxtral** for settings and a person's normal **Chat** view for transcripts and controls. See the [Voxtral guide](/docs/guides/voxtral). # v5 Release Status (/docs/getting-started/v5-alpha) The npm examples on these pages pin **5.1.0**. The npm registry reported `latest=5.1.0` and `alpha=5.0.0-alpha.8` on 2026-09-29. Docker Hub listed only its moving `latest` tag, so the Docker image version could not be pinned to the npm release. Choose `alpha` only for deliberate prerelease work. The example version is pinned; the registry tags above were checked on 2026-09-29. Generated API pages describe the package build that produced them, so verify exact method routes and return shapes against the version you deploy. ## Install v5.1.0 [#install-v510] Easy API: ```bash npx @open-wa/wa-automate@5.1.0 --session-id quickstart --host 127.0.0.1 --port 8080 ``` Custom code: ```bash npm install @open-wa/wa-automate@5.1.0 @open-wa/driver-puppeteer@5.1.0 ``` Docker currently publishes only its moving `latest` tag; Docker image parity with npm `5.1.0` could not be pinned from the tag list checked on 2026-09-29. Use the [quick start](/docs/getting-started/quickstart), [Docker](/docs/getting-started/docker), or [Custom code](/docs/getting-started/custom-code) page for the complete flow. A docs page returning HTTP 200 does not establish WhatsApp authentication; inspect `/health` and require `connected: true` plus `session.ready: true` before sending. ## What v5 includes [#what-v5-includes] * A monorepo package line with the runtime, API, schema, browser drivers, plugins, and integrations separated into packages. * `createClient` for applications that own the embedded runtime, with the exported `Client` facade for message listeners and methods. * HTTP Easy API commands, generated API docs, and runtime event delivery through the supported server surface. * Separate driver packages so an embedded application can choose Puppeteer or another supported driver. * Config-driven plugins through `plugins` and `pluginConfig`, including the webhook and Chatwoot integrations. ## Prerelease tags [#prerelease-tags] The `alpha` dist-tag currently points to `5.0.0-alpha.8`. It is a separate prerelease selection and can change independently of `latest`; it is not the command used by the supported-release onboarding pages. If you intentionally test it, record the exact package and image tag with your report and use a disposable account and profile. ```bash npx @open-wa/wa-automate@alpha --session-id alpha-test --port 8080 ``` ## v4 archive [#v4-archive] Existing v4 applications can continue to use the archived npm package: ```bash npx @open-wa/wa-automate@4.76.0 ``` Docker Hub did not list a versioned v4 image tag when checked on 2026-09-29. Keep v4 configuration, examples, and session profiles separate from v5. The v4 npm package remains available for migration work; new examples on this site use `5.1.0`. ## Migration notes [#migration-notes] 1. Start a separate v5 session with Easy API and authenticate a disposable WhatsApp account. 2. Check `/health` until `connected` and `session.ready` are both true. 3. Exercise the method, session recovery, plugin, and webhook flows your application needs. 4. Move to embedded `createClient` only when your application must own the browser and session lifecycle; use SocketClient for a remote consumer of Easy API. ## Legal and account safety [#legal-and-account-safety] This is unofficial software and is not affiliated with, authorized by, maintained by, sponsored by, or endorsed by WhatsApp or its affiliates. Automating WhatsApp can violate its terms of service, and account restrictions or bans are possible. Protect session profile directories, respect rate limits, and use a separate account while evaluating a new package or prerelease. # AI Agent Patterns (/docs/guides/ai-agent-patterns) # AI Agent Patterns [#ai-agent-patterns] This guide covers common architecture patterns for connecting LLMs to WhatsApp through open-wa. Snippets marked as runnable include setup or local stubs. Snippets marked as patterns are meant to show structure, not a complete application. ## Application-Provided AI Helpers [#application-provided-ai-helpers] The examples do not assume an official LLM, vision, or speech-to-text SDK. In your app, provide helpers with the same shape, then replace the stub bodies with your own provider calls. ```ts type ChatTurn = { role: 'user' | 'assistant'; content: string; }; async function callLLM(input: string | ChatTurn[] | unknown): Promise { void input; return 'Thanks, I received your message.'; } async function callVisionLLM(imageDataUrlOrBase64: string, mimeType?: string): Promise { void imageDataUrlOrBase64; return `Image received${mimeType ? ` as ${mimeType}` : ''}.`; } async function callSTT(audioDataUrlOrBase64: string): Promise { void audioDataUrlOrBase64; return 'Transcribed voice note text.'; } ``` Expected behavior with the stubs: a message comes in, the bot sends a deterministic placeholder response, and no external AI service is called. Add your own logging around each helper before replacing the stubs so you can see input, response, and provider errors. ## The Conversation Loop Pattern [#the-conversation-loop-pattern] The simplest AI agent receives messages, sends them to an LLM helper, and replies with the response. ```ts import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; type ChatTurn = { role: 'user' | 'assistant'; content: string; }; async function callLLM(input: string | ChatTurn[]): Promise { void input; return 'Thanks, I received your message.'; } const runtime = await createClient({ sessionId: 'ai-bot', driver: new PuppeteerDriver(), headless: false, }); const client = new Client({ client: runtime, transport: runtime.getTransport() }); await client.start(); client.onMessage(async (message) => { if (message.type !== 'chat' || message.fromMe) return; const response = await callLLM(message.body); await client.sendText(message.from, response); }); ``` Expected behavior: when a user sends a text message, the bot replies with the helper response. Messages from the bot itself and non-text messages are ignored. ### Key Decisions [#key-decisions] * **When to respond**: Filter by message type, sender, group membership, or keywords * **What to skip**: System messages, media, status updates, your own messages * **How to reply**: `sendText` for text, `sendImage` for generated images, `reply` for quoted responses ## Context Management [#context-management] LLMs need conversation context to maintain coherent multi-turn conversations. ### Sliding Window [#sliding-window] Pattern snippet: this assumes `client` and `callLLM` come from your application setup. ```ts type ChatTurn = { role: 'user' | 'assistant'; content: string; }; const contextWindow = new Map(); client.onMessage(async (message) => { const chatId = message.from; const history = contextWindow.get(chatId) ?? []; history.push({ role: 'user', content: message.body }); if (history.length > 20) { history.splice(0, history.length - 20); } const response = await callLLM(history); history.push({ role: 'assistant', content: response }); contextWindow.set(chatId, history); await client.sendText(chatId, response); }); ``` Expected behavior: each chat keeps its own rolling context. After more than 10 user and assistant exchanges, older turns are dropped before the next LLM call. ### Use getMessagesForLLM [#use-getmessagesforllm] Pattern snippet: if this generated Easy API method is available in your version, it can provide WhatsApp-native context. Its second argument is `last`, not `count`; provide your own `callLLM` helper for the returned message shape. Check the method reference before using it on an embedded client. ```ts client.onMessage(async (message) => { const recentMessages = await client.getMessagesForLLM(message.chatId, 20); const response = await callLLM(recentMessages); await client.sendText(message.from, response); }); ``` Expected behavior: the bot asks open-wa for recent chat context, sends that context to your helper, then replies to the same chat. ## Rate Limits and Safety [#rate-limits-and-safety] WhatsApp controls delivery and account restrictions on the provider side. A local limiter controls your own queue, but it cannot guarantee delivery or prevent an account restriction. ### Implementing Rate Limiting [#implementing-rate-limiting] Install the limiter in your application. This documentation example does not add dependencies to the open-wa repo. ```bash npm install bottleneck ``` Runnable snippet, assuming `client` and `callLLM` are defined as shown above: ```ts import Bottleneck from 'bottleneck'; const limiter = new Bottleneck({ minTime: 15000, maxConcurrent: 1, }); client.onMessage(async (message) => { if (message.type !== 'chat' || message.fromMe) return; try { const response = await callLLM(message.body); await limiter.schedule(() => { console.log('ai-agent: sending rate-limited reply', { chatId: message.from }); return client.sendText(message.from, response); }); } catch (error) { console.error('ai-agent: queued reply failed', { chatId: message.from, error }); } }); ``` Expected behavior: model calls run before the global outbound limiter, and outgoing replies start at least 15 seconds apart within this process. The catch block handles both provider rejection and queue failure; this schedule is not a WhatsApp safety guarantee and does not preserve per-chat ordering by itself. ### Ban Risk Profile [#ban-risk-profile] Provider restrictions can reflect volume, repeated content, recipient consent, account context, or signals that are not documented by open-wa. Keep consent, opt-out state, queue depth, and retry ownership in your application, and pause the workflow when the provider rejects sends. Random timing and an account's age are observations or assumptions, not controls this guide can promise. ## Media in LLM Pipelines [#media-in-llm-pipelines] ### Processing Incoming Images [#processing-incoming-images] Pattern snippet: `callVisionLLM` is an application-provided helper. Keep it as a stub until you wire your chosen vision provider. ```ts client.onMessage(async (message) => { if (message.type !== 'image') return; const mediaData = await client.decryptMedia(message); const base64 = mediaData.split(',')[1] ?? mediaData; const description = await callVisionLLM(base64, message.mimetype); await client.reply(message.from, `Image: ${description}`, message.id); }); ``` Expected behavior: The bot receives and decrypts an image message. The vision helper returns text. The bot replies with the description. ### Processing Voice Notes [#processing-voice-notes] Pattern snippet: `callSTT` and `callLLM` are application-provided helpers. Keep them as stubs until you wire your chosen speech and LLM providers. ```ts client.onMessage(async (message) => { if (message.type !== 'audio' && !message.mimetype?.includes('ogg')) return; const mediaData = await client.decryptMedia(message); const transcription = await callSTT(mediaData); const response = await callLLM(transcription); await client.sendText(message.from, response); }); ``` Expected behavior: The bot receives and decrypts a voice note. The speech helper returns text. The LLM helper creates a response, and the bot sends it. ### Sending Generated Media [#sending-generated-media] Pattern snippet: `sendImage` on the embedded client uses `sendImage(to, media, filename?, options?)`. Put captions and quote IDs in the options object. ```ts const imageDataUrl = 'data:image/png;base64,...'; const imageFilename = 'ai-generated.png'; const imageCaption = 'Generated image'; await client.sendImage(message.from, imageDataUrl, imageFilename, { caption: imageCaption }); ``` Expected behavior: the image is sent to the incoming chat with `ai-generated.png` as the attachment filename and `Generated image` as the caption. Use `sendPtt` on the embedded Client for voice-note output, supplying prepared audio such as Ogg Opus. ## Group Chats vs Direct Messages [#group-chats-vs-direct-messages] ### Detecting the Chat Type [#detecting-the-chat-type] Use this pattern in your message handler when group behavior must differ from direct-message behavior. ```ts client.onMessage(async (message) => { const isGroup = message.from.includes('@g.us'); const isDirect = message.from.includes('@c.us'); if (isGroup) { if (!message.body.includes('@botname')) return; } if (isDirect) { console.log('ai-agent: direct message received', { chatId: message.from }); } }); ``` Expected behavior: group messages only continue when the bot is mentioned. Direct messages continue through the direct-message branch and can be logged or handled separately. ### Group-Specific Considerations [#group-specific-considerations] * Respond only when `@botname` detection finds a mention * Respect group admin rules * Be aware of group size and message volume * Use group-specific context windows when necessary ## Interactive Messages [#interactive-messages] ### Buttons and Lists [#buttons-and-lists] Use these sends when you want your agent to offer fixed choices instead of free-form text. These are generated Easy API methods: `sendButtons` and `sendListMessage` are deprecated and require an Insiders license, and the current embedded `Client` does not declare them. The alias `sendList` refers to `sendListMessage` on surfaces that expose aliases. ```ts await client.sendButtons(message.from, 'Choose an option:', [ { id: 'opt1', body: 'Option 1' }, { id: 'opt2', body: 'Option 2' }, { id: 'opt3', body: 'Option 3' }, ]); await client.sendList(message.from, 'Select from the list:', 'Choose', [ { title: 'Section 1', rows: [{ id: 'r1', title: 'Row 1' }] }, ]); ``` Expected behavior: the chat receives either buttons or a list. Your handler must still process the response message type. ### Handling Button and List Responses [#handling-button-and-list-responses] Pattern snippet: branch on the response type and pass the selected ID into your own business logic. ```ts client.onMessage(async (message) => { if (message.type === 'buttons_response') { const selectedId = message.selectedButtonId; console.log('ai-agent: button selected', { selectedId }); } if (message.type === 'list_response') { const selectedId = message.listResponse?.selectedRowId; console.log('ai-agent: list row selected', { selectedId }); } }); ``` Expected behavior: button and list replies are logged with the selected ID. Replace the logs with your own handler once you know the menu shape. ## Concurrent Message Handling [#concurrent-message-handling] When multiple messages arrive simultaneously, handle them without overwhelming your LLM API or WhatsApp. Install the queue in your application: ```bash npm install p-queue ``` Runnable snippet, assuming `client` and `callLLM` are defined as shown above: ```ts import PQueue from 'p-queue'; const queue = new PQueue({ concurrency: 3 }); client.onMessage(async (message) => { if (message.type !== 'chat' || message.fromMe) return; void queue.add(async () => { console.log('ai-agent: processing queued message', { chatId: message.from }); const response = await callLLM(message.body); await client.sendText(message.from, response); }).catch((error) => { console.error('ai-agent: queued message failed', { chatId: message.from, error }); }); }); ``` Expected behavior: incoming messages enter the queue immediately, but only three combined LLM-and-send jobs run at once. The rejection handler prevents an unobserved queued failure; this compact example does not provide per-chat ordering or a separate outbound schedule. ### Queue Configuration [#queue-configuration] * **concurrency**: Maximum parallel queued jobs in this compact example; use a separate LLM queue when send pacing must not occupy a worker * **timeout**: Maximum time per message, for example 30 seconds * **retry**: Retry failed calls with exponential backoff in your own queue wrapper ## Complete Runnable Skeleton [#complete-runnable-skeleton] Install the runtime, driver, and queue package in your application before you use this skeleton: ```bash npm install @open-wa/wa-automate@5.1.0 @open-wa/driver-puppeteer@5.1.0 p-queue ``` Save the following JavaScript as `ai-agent.mjs`. The local `callLLM` stub returns a placeholder response, so the example does not call an AI provider. ```js import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; import PQueue from 'p-queue'; async function callLLM(input) { void input; return 'Thanks, I received your message.'; } // At most two model calls run at once across different chats. const llmQueue = new PQueue({ concurrency: 2 }); // One process-local queue starts at most one outbound task every 15 seconds. // This controls local scheduling, not provider acceptance or account status. const sendQueue = new PQueue({ concurrency: 1, intervalCap: 1, interval: 15000, strict: true }); const contextWindow = new Map(); const chatTails = new Map(); function enqueueForChat(chatId, task) { const previous = chatTails.get(chatId) ?? Promise.resolve(); const next = previous.catch(() => undefined).then(task); chatTails.set(chatId, next); const cleanup = () => { if (chatTails.get(chatId) === next) chatTails.delete(chatId); }; void next.then(cleanup, cleanup); return next; } const runtime = await createClient({ sessionId: 'ai-bot', driver: new PuppeteerDriver(), headless: true, }); const client = new Client({ client: runtime, transport: runtime.getTransport() }); await client.start(); client.onMessage(async (message) => { if (message.type !== 'chat' || message.fromMe) return; const isGroup = message.from.includes('@g.us'); if (isGroup && !message.body.includes('@bot')) return; const job = enqueueForChat(message.from, async () => { const history = [ ...(contextWindow.get(message.from) ?? []), { role: 'user', content: message.body }, ]; if (history.length > 20) { history.splice(0, history.length - 20); } const response = await llmQueue.add(() => callLLM([...history])); const sendResult = await sendQueue.add(() => { console.log('ai-agent: sending response', { chatId: message.from }); return client.sendText(message.from, response); }); if (sendResult === false) { throw new Error('WhatsApp did not accept the send request'); } history.push({ role: 'assistant', content: response }); contextWindow.set(message.from, history); }); void job.catch((error) => { console.error('ai-agent: message job failed', { chatId: message.from, error }); }); }); ``` Run it with `node ai-agent.mjs` and complete the normal client authentication when the browser opens. Expected behavior: The bot receives text messages and ignores group messages that do not mention `@bot`. Two `llmQueue` workers can call the model at once across different chats. `sendQueue` runs one send task at a time and starts at most one every 15 seconds in this process. `chatTails` waits for the previous model-and-send job for the same chat, preserving the order in which that chat's callbacks enter this process and keeping its context updates sequential. A rejected model or send reaches `job.catch`, where the application logs it; the next queued job for that chat still runs. A local interval is not a delivery or account-safety guarantee. ## Related [#related] * **[MCP Integration](/docs/guides/mcp)** - Let AI agents use WhatsApp via MCP * **[Rate limits](/docs/guides/rate-limits)** - WhatsApp rate limit details * **[Media handling](/docs/guides/media)** - Working with images, audio, and documents # Authentication and session recovery (/docs/guides/authentication-flow) # Authentication and session recovery [#authentication-and-session-recovery] open-wa automates WhatsApp Web through a browser driver. On first launch, scan the QR code with WhatsApp's Linked Devices screen and keep the runtime running until authentication and client readiness complete. The [quick start](/docs/getting-started/quickstart) shows the complete first message path. ## Pair a session [#pair-a-session] The Easy API CLI's QR is rendered in its local terminal. The current published `@open-wa/wa-automate@5.1.0` CLI does not forward link-code configuration, so use QR pairing with that CLI. In an embedded application, set `createClient`'s `linkCode` to the phone's full international number using digits only, then listen for `launch.auth.linkCode.generated` before calling `client.start()`; read the code from `event.details.linkCode`. Follow the complete [link-code login flow](/docs/getting-started/link-code), which covers the phone steps and keeps the generated code in the local terminal. For the published CLI, see [Easy API setup](/docs/getting-started/easy-api) and [configuration options](/docs/guides/configuration-and-cli) before starting. Do not print link codes or QR values to shared logs, webhooks, or support tools. ## Persist the session [#persist-the-session] The browser profile holds WhatsApp authentication state. The runtime derives a profile directory from the session ID by default; set `userDataDir` when you need a specific path and keep that directory private, persistent, and unique to the session. ```ts const runtime = await createClient({ sessionId: 'support', userDataDir: './private/sessions/support', driver: new PuppeteerDriver(), }); ``` Set restrictive file permissions on the profile, exclude it from version control and backups shared across accounts, and protect it like a credential. Setting `ephemeral: true` uses a temporary profile that is discarded when the process exits, so the next run may require pairing again. ## Check readiness [#check-readiness] An HTTP response from `/health` means the Easy API process answered. Wait until its `connected` and `session.ready` fields are both `true` before sending messages. The current `/health` response can include QR and diagnostic data and is public, so keep it on loopback or a private network. ```bash curl -sS http://127.0.0.1:8080/health ``` See [health and readiness](/docs/getting-started/quickstart#3-check-readiness) for the expected fields and recovery diagnostics. A process restart does not prove that WhatsApp reconnected; check readiness again after every restart. ## Recover a session [#recover-a-session] If the browser or Node process exits, restart with the same session ID and profile directory. The runtime attempts to restore authentication from that profile. If WhatsApp logged the device out, the profile is missing or damaged, or authentication times out, pair again and investigate the session state before retrying application work. If you suspect that someone copied the profile, log out the linked device from WhatsApp, stop the runtime, replace the profile, and authenticate a new session. Do not delete a profile as a routine recovery step because it removes the saved authentication state. ## Runtime events [#runtime-events] Use `client.ready` for the point at which the client can accept commands and `session.connection.disconnected` to observe a later disconnect. QR and link-code lifecycle events contain authentication material; see the [session events guide](/docs/guides/session-events) for the safe event surface. ```ts runtime.events.on('client.ready', ({ sessionId }) => { console.log(`Session ${sessionId} is ready`); }); runtime.events.on('session.connection.disconnected', ({ details }) => { console.warn('Session disconnected', details?.reason); }); ``` ## Related [#related] * [Easy API setup](/docs/getting-started/easy-api) for CLI installation and API-key configuration * [Security and deployment](/docs/operations/security-and-deployment) for network and credential boundaries * [Detect logouts](/docs/operations-and-troubleshooting/detect-logouts) for connection-state recovery # Calls (/docs/guides/calls) # Calls [#calls] WhatsApp audio calling is a licensed preview included with active Insiders, Restricted and Premium licenses, including Gumroad purchases. Patches Only does not include calling. Use the latest package and configure your license key; OpenWA activates calling after pairing. Camera calls and provider adapters are still in development. ## Answer with a greeting and record the caller [#answer-with-a-greeting-and-record-the-caller] Install the main package, which includes its browser driver and audio decoder: ```sh npm install @open-wa/wa-automate@latest ``` Create a client with your license key inline, then handle incoming calls: ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ licenseKey: 'YOUR_CALLING_LICENSE' }); await client.onIncomingCall(async call => { await call.accept('greeting.pcm', 'received.pcm'); }); ``` The first argument supplies the microphone audio; the second receives the caller's audio. File paths resolve on the machine running this client. Encoded audio files are detected automatically. Raw `.pcm` defaults to signed 16-bit little-endian, 16 kHz, mono. Change the filenames to `.wav` to play a WAV greeting and save a WAV recording. OpenWA prepares the source and recording before answering. The greeting begins when the call becomes active, plays once, and then sends silence while the call stays open. Returning from your callback keeps the call media alive. When the peer hangs up, OpenWA stops the source, closes its resources and finishes the recording. Existing recordings are preserved by choosing a new filename. There is no capability-check step. Without an enabled license or supported browser, OpenWA logs the reason and calling actions return an unavailable result; your messaging session continues. ## Use devices, URLs or streams [#use-devices-urls-or-streams] To answer with a conversational AI assistant, use the official [Voxtral plugin](/docs/guides/voxtral). It manages live transcription, answers, and speech from your Easy API configuration. Omit both arguments to use the browser running the WhatsApp session's microphone and speaker: ```ts await client.onIncomingCall(call => call.accept()); ``` Device access needs working browser permissions and devices. Use files or streams for a headless server without physical devices. `DEFAULT_MIC` and `DEFAULT_SPEAKER` are available when you want to spell out a default in a mixed-source call. ```ts import { DEFAULT_SPEAKER } from '@open-wa/wa-automate'; await client.onIncomingCall(async call => { await call.accept('https://example.com/greeting.mp3', DEFAULT_SPEAKER); }); ``` The Node runtime decodes supported HTTP audio and HLS URLs with the included decoder. HLS can introduce buffering latency, so a live audio stream is usually a better choice for a conversation. Application streams use the same positional API. `microphone` is a Web `ReadableStream` and `speaker` is a Web `WritableStream`: ```ts await client.onIncomingCall(async call => { await call.accept(microphone, speaker); }); ``` Raw application input and received output use 16-bit little-endian PCM at 16 kHz mono. Encoded input is detected from its container header. OpenWA owns the stream readers and writers for the attachment and releases them when its call ends. Its Effect scopes and decoders are internal; applications don't need to set up Effect or attachment IDs. Pass `null` to disable a role: ```ts await call.accept('greeting.wav', null); // Send a greeting without receiving audio. await call.accept(null, 'received.wav'); // Receive and record without sending audio. ``` For explicit raw formats, looping or authenticated URLs, use the options object: ```ts await call.accept({ microphone: { kind: 'file', path: 'hold-music.pcm', raw: { encoding: 'pcm16le', sampleRate: 48_000, channels: 2 }, loop: true, }, speaker: { kind: 'file', path: 'received.wav' }, }); ``` A compatible `wss://` source or sink announces its format using the OpenWA audio protocol. Arbitrary provider sockets need a protocol adapter; a URL alone does not identify a proprietary wire format. ## Start, reject or end a call [#start-reject-or-end-a-call] ```ts await client.startCall('RECIPIENT_NUMBER', 'greeting.wav', 'received.wav'); await client.onIncomingCall(async call => { await call.reject(); }); await client.onCallState(async call => { console.log(call.direction, call.state); if (call.terminal) console.log('Call ended; its media has closed.'); }); ``` Actions return `{ ok: true, status: 'requested' }` when the request was accepted. Watch `onCallState` for the observed call state. A requested or active state does not itself prove that a person heard your audio. OpenWA never automatically redials an uncertain request. Use the current `Call` object from a callback or `await client.getActiveCall()` for controls: ```ts const call = await client.getActiveCall(); if (call) { await call.mute(); await call.unmute(); await call.clearAudio(); await call.setMedia({ microphone: 'next-prompt.wav', speaker: 'next-recording.wav' }); await call.end(); } ``` `clearAudio()` clears outgoing audio; clearing a finite greeting stops that greeting. `setMedia()` prepares the new attachment before switching and then releases the old one. There is one media writer per call. Update sources from the client that owns the attachment. To add a separate receive-only recording, use an observer: ```ts const observer = await call.observeAudio('audit.wav'); if (observer.ok) await observer.stop(); ``` Observers don't take over the microphone. They close automatically at call end; a slow or failed observer closes its own sink. ## Connect from another process or a browser [#connect-from-another-process-or-a-browser] The remote helper opens files and streams on the caller's machine: ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://127.0.0.1:8080'); await client.onIncomingCall(call => call.accept('greeting.wav', 'received.wav')); ``` The Node helper uses files, URLs and application streams. For physical devices, use the dashboard's browser. Browser clients use a `File`, `Blob`, HTTP audio URL or device as input, and their speaker or a `WritableStream` as output. Browser URLs must allow CORS access. The helper handles media admission and its connection automatically. An API on another machine uses its configured API authentication and an HTTPS/WSS deployment. Direct HTTP media descriptors resolve on the API host; they don't access the caller's filesystem. Reloading or disconnecting the helper releases its attachment. Reconnect explicitly and attach fresh sources using a current `Call` object. ## Try the shipped Calls dashboard [#try-the-shipped-calls-dashboard] Start the Easy API with your license and dashboard enabled: ```sh npx @open-wa/wa-automate@latest --license-key YOUR_CALLING_LICENSE --dashboard ``` Open `/dashboard/calls` on the API's address. Search for a contact by name, phone number or LID and select the result, or enter an international number or full contact ID and select it. Choose **Bundled OpenWA greeting**, select **Record received audio** and choose **Start audio call**. The speech starts after the peer answers. End the call from either side; the page shows the terminal state and offers **Download received audio**. For an incoming call, choose **Answer** or **Reject**. Choose **My microphone** for a conversation, **An audio file** for a local file, or **An HTTP audio URL** for a supported URL. The browser owns these local sources and asks for device permission when needed. You can also open an individual conversation in **Chat** and choose **Call** in its header. It uses the same audio settings. During a call, a compact control at the top of the dashboard shows ringing, connection progress and connected time. Hover over it or click to expand it, then answer, reject, mute or end the call from any page. Navigation preserves the call, audio settings and completed recording; reloading the browser closes its media attachment. The session's licence menu lists enabled and disabled features. Applications can read the same access list with `await client.getLicenseFeatures()`, which returns `status`, `keyType`, `expiresAt` and a boolean `features` map. For example, `features['calls.audio']` reports licensed call audio access. This is optional information for your UI; calling actions handle admission themselves, so you don't need a capability check before making a call. ## When media stops [#when-media-stops] Expected license, stale-call and source failures return `{ ok: false, status, reason }` and log an actionable message. Authentication and unexpected programming failures remain ordinary API errors. Audio pauses and buffer overload do not hang up the call. The microphone sends silence while it waits for more data, then resumes when the same stream supplies it. A finished file leaves the microphone silent and received audio keeps playing or recording. If a microphone source fails, the call and received-audio sink stay open; attach a new source with `call.setMedia(...)`. Other media failures also keep the call open by default, although a failed attachment must be replaced. Applications can explicitly choose `onMediaFailure: 'end'` to hang up on a source or sink failure. Browser recovery and expired calling access close devices, sockets and recordings; `onCallState` can report `call.media.state === 'ended'` with a reason while the native call's state remains separate. Old Call objects cannot control a new browser generation. Terminal snapshots include `call.endReason` with a `code`, readable `message` and `source` (`local`, `media`, `license`, `session` or `whatsapp`). The dashboard shows that message in the floating call control and recent call states. Media errors retain the audio source, queue or WebSocket failure that caused the shutdown. If WhatsApp ends a call without a reported cause, the message says so; it does not assume that the peer hung up or that the network failed. Live input keeps at most 200 ms of queued audio. If processing falls behind, bounded audio queues discard samples and log the discarded duration while keeping the call open. This can produce an audible gap; it prevents stale audio from building a growing delay. Timestamped received audio inserts zero PCM samples for missing intervals in recordings, so later speech keeps its place on the timeline. Those silence samples preserve timing, not speech that was lost. Live speaker playback catches up instead of playing the missing interval. File inputs retain their playback clock, and finished greetings leave the call open. The current dashboard carries audio. Incoming video offers are labelled and can be answered with audio; live camera and received video are not available in this dashboard yet. See the [generated calls reference](/docs/reference/client/calls) for the wire methods and exact parameter schemas. # Understanding Chat IDs (/docs/guides/chatid-primer) # Understanding Chat IDs [#understanding-chat-ids] Every conversation is addressed with a chat ID. Use the identifier returned by an event or lookup when you have one; only construct an ID from a phone number when you are deliberately addressing a direct contact. ## Accepted API formats [#accepted-api-formats] The current `ChatId` schema accepts these forms: | Form | Meaning | Example | | --------------------------- | ------------------------------------- | ------------------------------ | | `@c.us` | A direct-contact JID | `447700900000@c.us` | | `(-)?@g.us` | A group JID | `447123456789-1445627445@g.us` | | `@lid` | A numeric LID JID accepted by the API | `12345678901234@lid` | The schema is a validation contract for method inputs. It does not prove that every WhatsApp account, device, or incoming payload uses every form shown here. In particular, a LID is an opaque WhatsApp identifier: this guide does not claim that all LIDs are colon-separated or that a LID can be derived from a phone number. ## Phone numbers and direct-contact IDs [#phone-numbers-and-direct-contact-ids] For a direct contact, use the country code, remove the leading `+`, spaces, dashes, and parentheses, then append `@c.us`: ```ts function phoneToChatId(phone: string): string { const digits = phone.replace(/\D/g, ''); return `${digits}@c.us`; } phoneToChatId('+44 7700 900000'); // '447700900000@c.us' ``` The runtime also has a codec for bare numeric input. It preserves already formatted IDs; for bare digits it applies heuristic rules, including a special case for 18 digits as a group ID and 14 digits (except numbers beginning with `62`) as a LID. Prefer an explicit suffix when you know the identifier because those heuristics cannot establish what a real WhatsApp account represents. ## Group IDs [#group-ids] Group IDs are assigned by WhatsApp. Keep the full value exactly as returned by `message.from`, `message.chatId`, a group lookup, or a group method; do not rebuild one from a group name, participant numbers, or a timestamp assumption. ```ts client.onMessage((message) => { if (message.isGroupMsg) { console.log('Group ID:', message.chatId); } }); ``` An invite URL is an invite token, not a group ID. If the workflow is to join a group, pass the URL to [`joinGroupViaLink`](/docs/reference/client/groups#joingroupvialink) and keep the returned identifier only after checking the method's result. The method is license-gated and can return a failure value, so a successful HTTP request alone is not proof that a group was joined. ```ts const result = await client.joinGroupViaLink( 'https://chat.whatsapp.com/REDACTED_INVITE_TOKEN' ); if (typeof result === 'string') { console.log('Joined group:', result); } else { console.error('The invite could not be resolved:', result); } ``` ## Incoming fields and LIDs [#incoming-fields-and-lids] Message events expose the conversation through fields such as `from` and `chatId`. Representative sanitized payloads for the documented contact and group forms look like this: ```json { "from": "447700900000@c.us", "chatId": "447700900000@c.us", "isGroupMsg": false } ``` ```json { "from": "447123456789-1445627445@g.us", "chatId": "447123456789-1445627445@g.us", "isGroupMsg": true } ``` The supported source and schema do not establish one universal incoming LID representation beyond the numeric `@lid` input accepted by the current API. When an event contains a LID, pass the exact `from` or `chatId` value to the next method and record the release and event payload before documenting a new shape. Do not split, rewrite, or infer a phone number from it. ## Common mistakes [#common-mistakes] | Mistake | Wrong | Correct | | ------------------------------------ | ----------------------------- | ----------------------------------- | | Missing country code | `7700900000@c.us` | `447700900000@c.us` | | Wrong suffix for a contact | `1234567890@g.us` | `1234567890@c.us` | | Display name instead of ID | `John Doe@c.us` | `447700900000@c.us` | | Keeping the `+` sign | `+447700900000@c.us` | `447700900000@c.us` | | Treating an invite URL as a group ID | `ABCdef...@g.us` | Resolve it through the group method | | Reconstructing an opaque ID | A guessed LID or group suffix | Reuse the returned identifier | ## Related [#related] * **[Messages guide](/docs/guides/messages):** Send and receive messages with chat IDs * **[Groups guide](/docs/guides/groups):** Work with group chats and invite links * **[Client messages reference](/docs/reference/client/messages):** Exact method parameters and return types # Config Explorer (/docs/guides/config-explorer) # Config Explorer [#config-explorer] open-wa has one configuration schema. The runtime exposes each schema field in the config file, while the CLI and environment adapters cover smaller, explicitly supported subsets: * in a config file (`wa.config.json` / `wa.config.mjs`), by its key * as a CLI flag where the v5 CLI parser supports that option * as an environment variable where the environment loader supports that key Use the explorer below to search every option, filter by group, and switch between the three forms. The table and copy-paste snippet update to match the format you pick. Options are grouped by area (browser, session, server, logging, and so on) from the config schema itself. Nested fields are shown under their full config path, and a `config file only` label means the selected adapter does not accept that field. For the complete source-backed option reference, including full types and defaults, see [Configuration and CLI](/docs/guides/configuration-and-cli#full-configuration-reference). The explorer does not invent adapters. For example, `mcp.enabled` is a nested config-file field, while `WA_MCP` is the supported environment form for the whole MCP object; the current CLI does not parse `--mcp`. A few supported flags also have short aliases, such as `-p` for `--port`. This page is generated from `@open-wa/config`'s schema plus the v5 CLI and environment adapters, so the displayed forms describe what each runtime path actually accepts. # Config schema (/docs/guides/config-schema) # Config schema [#config-schema] This page exposes the current configuration surface from the schema/config source of truth instead of relying only on prose summaries. Start with **[Configuration and CLI](/docs/guides/configuration-and-cli)** for the practical guidance, then use this page when you need the concrete field surface and type details. ## Primary config object [#primary-config-object] ## Strict config [#strict-config] ## Partial config [#partial-config] ## Deep partial config [#deep-partial-config] # Managing Secrets in Config (/docs/guides/config-secrets) Keep secrets outside committed source, load them into `process.env` before the config file is evaluated, and pass each value through the config key that actually consumes it. The examples target `@open-wa/wa-automate@5.1.0` and use `WA_*` names for runtime settings. ## Load a `.env` file explicitly [#load-a-env-file-explicitly] The open-wa config loader reads `process.env`; it does not make a project `.env` file appear automatically. Create a local npm project and install the runtime, the webhook plugin referenced below, and a `.env` loader: ```bash npm init -y npm install @open-wa/wa-automate@5.1.0 @open-wa/integration-webhook@5.1.0 dotenv ``` Create `.env` next to `wa.config.mjs` and keep it out of version control: ```dotenv WA_API_KEY=replace-with-a-long-random-key WA_SESSION_ID=sales WA_PORT=8080 OPEN_WA_WEBHOOK_SECRET=replace-with-webhook-secret CHATWOOT_URL=https://chatwoot.example/app/accounts/1/inboxes/2 CHATWOOT_API_ACCESS_TOKEN=replace-with-chatwoot-token OPEN_WA_PUBLIC_URL=https://openwa-api.example ``` Load the file in `wa.config.mjs`, then start the supported release with that config: ```js // wa.config.mjs import 'dotenv/config'; const required = (name) => { const value = process.env[name]; if (!value) throw new Error(`${name} is required`); return value; }; export default { apiKey: required('WA_API_KEY'), sessionId: process.env.WA_SESSION_ID ?? 'sales', port: Number(process.env.WA_PORT ?? 8080), plugins: ['@open-wa/integration-webhook'], pluginConfig: { webhook: { url: 'https://your-app.example/webhooks/open-wa', events: ['message.received'], headers: { 'X-Webhook-Secret': required('OPEN_WA_WEBHOOK_SECRET'), }, }, }, }; ``` ```bash npx wa-automate --config ./wa.config.mjs --session-id sales --host 127.0.0.1 --port 8080 ``` The shared config loader reads `WA_API_KEY`, `WA_PORT`, and `WA_SESSION_ID`. The published `5.1.0` CLI applies its own session, host, and port defaults after that loader, so the command supplies those three flags explicitly. `OPEN_WA_WEBHOOK_SECRET` and `CHATWOOT_API_ACCESS_TOKEN` are application-level names; the config file maps them to the integration settings that consume them. ## Docker secrets and environment variables [#docker-secrets-and-environment-variables] For complete, digest-pinned run and Compose commands, see the [Docker guide](/docs/getting-started/docker). Compose reads `.env` values for interpolation but passes only mapped `environment` values into the container. Set `W_A_V=5.1.0` there to pin the package installed at startup. The container does not read Docker secret files by itself. If your platform mounts a secret under `/run/secrets`, use an entrypoint that reads that file, exports its value as `WA_API_KEY`, and then runs the open-wa CLI. Restrict access to both the secret file and the session profile. ## Plugin configuration shape [#plugin-configuration-shape] The top-level `plugins` array contains npm package names or local plugin paths. The `pluginConfig` object is keyed by each plugin's `meta.name`; the plugin validates and consumes the value under its own key. ### Webhook integration [#webhook-integration] Webhook delivery uses the `@open-wa/integration-webhook` plugin. Its supported config is an object under `pluginConfig.webhook`, with `url`, optional `events`, and optional request `headers`: ```js export default { plugins: ['@open-wa/integration-webhook'], pluginConfig: { webhook: { url: 'https://your-app.example/webhooks/open-wa', events: ['message.received'], headers: { 'X-Webhook-Secret': process.env.OPEN_WA_WEBHOOK_SECRET, }, }, }, }; ``` The top-level `webhook` config key is a separate string URL field. The current v5 CLI does not register delivery from `--webhook`; use the plugin shape above for webhook delivery and see **[Webhook payloads](/docs/client-and-integrations/webhook-payloads)** for the receiver contract. ### Chatwoot integration [#chatwoot-integration] Chatwoot uses the plugin's documented field names, not `instanceUrl`: Install the plugin package into the same npm project that installs the runtime: ```bash npm install @open-wa/integration-chatwoot@5.1.0 ``` ```js export default { plugins: ['@open-wa/integration-chatwoot'], pluginConfig: { chatwoot: { chatwootUrl: process.env.CHATWOOT_URL, chatwootApiAccessToken: process.env.CHATWOOT_API_ACCESS_TOKEN, apiHost: process.env.OPEN_WA_PUBLIC_URL, apiKey: process.env.WA_API_KEY, forceUpdateCwWebhook: true, }, }, }; ``` `chatwootUrl` contains the Chatwoot account URL and optional inbox path, `chatwootApiAccessToken` authenticates API calls, and `apiHost` is the public origin Chatwoot can reach. See **[Chatwoot integration](/docs/client-and-integrations/chatwoot)** for the schema and webhook route. ### Illustrative custom plugin [#illustrative-custom-plugin] `my-plugin` is an invented application example. Its fields have no meaning to open-wa unless your plugin defines and reads them: ```js export default { plugins: ['./plugins/my-plugin.mjs'], pluginConfig: { 'my-plugin': { apiKey: process.env.MY_PLUGIN_API_KEY, enabled: true, }, }, }; ``` Define and validate those fields in the plugin's own `configSchema`; do not copy this shape into a real integration without checking that integration's schema. ## Security rules [#security-rules] * Use environment variables or a secret manager for API keys, license keys, webhook secrets, and integration tokens. * Add `.env` and local secret directories to `.gitignore`; never commit session profiles or secret files. * Never log the complete config object. Log only booleans such as `hasKey: Boolean(config.apiKey)` and non-sensitive URLs. * Rotate a secret by changing the injected value and restarting the runtime or plugin so it is loaded again. * Treat the browser profile directory as secret material because it contains WhatsApp authentication state. ## Validate at startup [#validate-at-startup] Validate required application secrets before starting the runtime. Zod is one option for an application-owned config layer: ```ts import { z } from 'zod'; const configSchema = z.object({ apiKey: z.string().min(1), webhookSecret: z.string().min(1), }); const appConfig = configSchema.parse({ apiKey: process.env.WA_API_KEY, webhookSecret: process.env.OPEN_WA_WEBHOOK_SECRET, }); ``` The runtime validates its own config and plugin schemas separately. Application validation prevents a missing secret from becoming a partially configured integration. ## Related [#related] * **[Docker](/docs/getting-started/docker)**: complete port, profile, and secret-loading commands. * **[Configuration and CLI](/docs/guides/configuration-and-cli)**: config discovery and `WA_*` precedence. * **[Plugin security model](/docs/plugins/security-model)**: plugin boundaries. * **[Security and deployment](/docs/operations/security-and-deployment)**: production network and secret controls. # Configuration and CLI (/docs/guides/configuration-and-cli) # Configuration and CLI [#configuration-and-cli] Use this page to choose runtime settings, CLI flags, environment variables, and plugin configuration that match the way you start open-wa. ## Common config fields [#common-config-fields] Use the config object passed to `createClient()` or exported from `wa.config.*` for library/runtime behavior such as: * `sessionId`: stable session naming * `userDataDir`: persistent browser profile directory * `headless`: visible vs background browser * `useChrome` / `executablePath`: browser selection * `qrTimeout` / `authTimeout`: authentication timing * `maxQr`: limit QR emission count * `cacheEnabled`: page/runtime memory trade-off * `licenseKey`: feature unlocks where applicable * `proxyServerCredentials`: outbound proxy support * `linkCode`: link-code login for embedded `createClient` applications ## Full Configuration Reference [#full-configuration-reference] The reference below lists supported configuration fields, types, defaults, and descriptions. ## Example [#example] ```ts import { createClient } from '@open-wa/wa-automate'; const client = await createClient({ sessionId: 'sales', useChrome: true, headless: true, qrTimeout: 0, authTimeout: 60, }); ``` ## Config file discovery [#config-file-discovery] The CLI loads configuration through the shared config package before applying CLI overrides. By default, discovery starts from the current working directory. Discovery order is: 1. The `wa` key in `package.json` 2. `wa.config.js` 3. `wa.config.cjs` 4. `wa.config.mjs` 5. `wa.config.ts` 6. `wa.config.mts` 7. `wa.config.cts` 8. `wa.config.json` 9. `.warc` 10. `.warc.json` 11. `cli.config.js` legacy compatibility 12. `cli.config.json` legacy compatibility The first matching file wins. Use `--config ./path/to/config` when you want to load a specific file instead of relying on discovery. The config loader merges values in this order: 1. schema defaults 2. config file 3. `WA_*` environment variables 4. CLI flags 5. programmatic overrides, mainly for tests and embedding The config loader replaces arrays instead of concatenating them. A later `plugins` value replaces the earlier value. Config files can export a plain object or a function that returns an object. The config loader supports TypeScript config files. ## JSON/TS examples [#jsonts-examples] These config fields are available to embedded `createClient` applications. In the published Easy API CLI `5.1.0`, startup applies `host: 0.0.0.0` and `port: 8002` after config and environment loading, so pass `--host` and `--port` explicitly when starting the CLI. Use JSON when your config is static: ```json { "sessionId": "sales", "port": 8080, "host": "0.0.0.0", "apiKey": "replace-with-env-in-production", "headless": true, "qrTimeout": 0, "authTimeout": 60, "licenseKey": "YOUR-LICENSE-KEY" } ``` Use TypeScript or JavaScript when you need environment variables or computed values: ```ts import { defineConfig } from '@open-wa/config'; export default defineConfig({ sessionId: process.env.WA_SESSION_ID ?? 'sales', port: Number(process.env.WA_PORT ?? 8080), host: '0.0.0.0', apiKey: process.env.WA_API_KEY, userDataDir: process.env.WA_USER_DATA_DIR, headless: true, qrTimeout: 0, authTimeout: 60, licenseKey: process.env.WA_LICENSE_KEY, }); ``` In v5 config examples, use the `WA_*` environment names that the config loader reads. If your platform uses different secret names, map them in the config. Names such as `PORT` and `API_KEY` are not automatic runtime config variables. ## Env vars [#env-vars] Supply runtime config with `WA_*` environment variables. The loader converts snake-case names to config keys. For example, `WA_SESSION_ID` becomes `sessionId`. The config loader converts values when possible: * `true`, `1`, `yes`, and `on` become `true` * `false`, `0`, `no`, and `off` become `false` * numeric strings become numbers for numeric schema fields * the loader parses JSON values such as `{...}` or `[...]` for object and array fields Common runtime environment variables: | Environment variable | Config field | Notes | | -------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `WA_SESSION_ID` | `sessionId` | Stable session name | | `WA_PORT` | `port` | The shared config loader reads this value, but the published CLI `5.1.0` applies `port: 8002` after environment variables. Pass `--port` to choose another port. | | `WA_HOST` | `host` | The shared config loader reads this value, but the published CLI `5.1.0` applies `host: 0.0.0.0` after environment variables. Pass `--host` to choose another bind address. | | `WA_API_KEY` | `apiKey` | API authentication key | | `WA_USER_DATA_DIR` | `userDataDir` | Persistent browser profile directory | | `WA_LICENSE_KEY` | `licenseKey` | License key where applicable | | `WA_LINK_CODE` | `linkCode` | The shared config loader maps this variable. The published CLI `5.1.0` does not forward the field into startup; pass `linkCode` explicitly to embedded `createClient`. | | `WA_HEADLESS` | `headless` | Browser visibility | | `WA_USE_CHROME` | `useChrome` | Prefer local Chrome | | `WA_QR_TIMEOUT` | `qrTimeout` | QR wait time | | `WA_AUTH_TIMEOUT` | `authTimeout` | Auth wait time | | `WA_WEBHOOK` | `webhook` | Webhook target URL | | `WA_PLUGINS` | `plugins` | JSON array of plugin refs | | `WA_PLUGIN_CONFIG` | `pluginConfig` | JSON object keyed by plugin name | There are also compatibility aliases for a few browser-related settings, including `WA_DEBUG` for `devtools`, `WA_BROWSER_WS_ENDPOINT` for `browserWSEndpoint`, `WA_BYPASS_CSP` for `bypassCSP`, and `WA_USE_LIGHTPANDA` for `useLightpanda`. ## High-value CLI flags [#high-value-cli-flags] ```bash # networking (pass explicit values to override published CLI 5.1.0 defaults) --host "127.0.0.1" --port 8080 --api-key "your-secure-key" # session identity --session-id sales # runtime helpers --pm2 --tunnel # licensing --license-key "YOUR-LICENSE-KEY" # isolate explicit chat-triggered code/tool execution --sandbox-chats --sandbox-isolation container ``` Pass `linkCode` explicitly in an embedded `createClient` configuration. The shared config loader maps `WA_LINK_CODE`, but direct `createClient` calls do not load environment variables automatically. The published Easy API CLI `5.1.0` does not forward link-code settings and has no `--link-code` flag, so use its QR flow. Select a config file with `--config` when you need to choose it explicitly. `sandboxChats` applies only to code or tools explicitly sent through `executeInChatSandbox`; it does not create a Chromium session per chat or change ordinary message handlers. The boolean form uses the process policy, while the object form controls the isolation boundary and resource access: ```ts export default defineConfig({ sandboxChats: { isolation: 'container', // worker | process | container timeoutMs: 30_000, idleTimeoutMs: 300_000, memoryMb: 256, concurrency: 1, filesystem: 'none', // none | read-only | workspace network: 'none', // none | allowlist networkAllowlist: [], env: 'none', capabilities: ['sendText'], }, }); ``` Container mode is the strong boundary for hostile code. Process mode strips ambient globals, environment, and filesystem permission and applies time/memory/output limits, but Node's permission model is defense in depth rather than a complete security boundary. Worker mode is intended for availability isolation around trusted code. Worker and process policies fail closed if they request filesystem, network, or environment access; those host-access policies require container mode. ## Full CLI reference [#full-cli-reference] These are the major flags most users need when starting the Easy API from the command line. | Flag | Maps to | Description | | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--port 8080`, `-p 8080` | `port` | Port for the Easy API server. | | `--api-key "key"`, `--key "key"`, `-k "key"` | `apiKey` | Protects API requests. Use this before exposing the API beyond localhost. | | `--session-id sales` | `sessionId` | Names the session and keeps auth state separate from other sessions. | | `--webhook "https://..."`, `-w "https://..."` | `webhook` | Parsed as a webhook target URL, but current v5 source warns that CLI webhook registration parity is not restored. Use `@open-wa/integration-webhook` in `plugins` plus `pluginConfig.webhook` for source-backed delivery. | | `--pm2` | PM2 wrapper | Starts the CLI under PM2 instead of running directly. | | `--license-key "key"`, `-l "key"` | `licenseKey` | Supplies a license key for licensed features where applicable. | | `linkCode` in library config | `linkCode` | Embedded `createClient` in `5.1.0` supports link-code login. The published Easy API CLI does not forward this setting and has no `--link-code` flag. | | `--tunnel` | transitional | Legacy tunnel flag. v5 currently warns that tunnel setup parity is not restored. | | `--mcp` | unsupported by the current v5 CLI | The CLI does not parse this flag. Configure `mcp: { enabled: true }` in `wa.config.*`, keep `apiKey` enabled, and use `--config ./wa.config.mjs` when selecting that file explicitly. | | `--host "0.0.0.0"`, `-h "0.0.0.0"` | `host` | Local bind host for the Easy API server. | | `--sandbox-chats` | `sandboxChats: true` | Enables isolated execution for explicit per-chat code/tool calls. | | `--sandbox-isolation process` | `sandboxChats.isolation` | Selects `worker`, `process`, or `container`; use container for hostile code. | Other useful runtime flags include `--host`, `--headless`, `--headful`, `--use-chrome`, `--use-chromium`, `--use-lightpanda` (also `--lightpanda`), `--qr-timeout`, `--dashboard-port`, `--no-dashboard`, `--no-ezqr`, `--ephemeral`, `--sandbox-chats`, `--sandbox-isolation`, `--log-console`, `--log-level`, and `--verbose`. The CLI package installs without downloading browsers. Startup prefers installed Google Chrome by default. If Chrome and a previously selected browser are missing, the terminal asks which browser to download: * **Chrome for Testing** provides the highest compatibility, including video and media. * **Chromium** is the middle ground; proprietary video/audio codecs may be unavailable. * **Lightpanda** is lightweight for basic automation, with low compatibility and experimental WhatsApp support. It does not render pages or support video or screenshots. `--use-chrome`, `--use-chromium`, and `--use-lightpanda` select a browser directly, reuse it if available, and download it on demand if missing. Downloads are cached per user, outside the temporary `npx` installation, and the terminal choice is remembered for later runs. A non-interactive first start with no available browser requires an explicit browser flag. `browser.download: 'never'` disables downloads. After the next minor release, start the experimental Lightpanda Easy API with: ```bash npx @open-wa/wa-automate --lightpanda ``` Lightpanda mode downloads and caches the pinned 1.0.0 executable on first use, prepares a separate WhatsApp-capable copy, and enables the worker, service worker, and crypto bridges automatically. The original executable and existing installations are preserved. The normal terminal QR output and `/qr` endpoint remain available. Use `--qr-timeout 0` to keep waiting for a scan. A custom `lightpanda.executablePath` is still supported; that executable must honor the Chrome user agent override. This mode remains experimental. Custom native Debug runs reached authenticated library READY and loaded chats/contacts, but subsequent reload recovery failed with session takeover and initialization errors. The stock download has not been replaced. Reliable recovery, outbound delivery, long-session durability and full Chrome parity remain unconfirmed. Lightpanda has no graphical renderer, so screenshots, PDF rendering and Live Portal are unavailable. The controlled browser benchmark does not measure authenticated WhatsApp performance. The standalone `@open-wa/cli` entrypoint also accepts output-mode flags such as `--interactive`, `--non-interactive`, and `--output-mode plain`. Those affect terminal output before the runtime config is parsed. ## CLI-to-schema mapping [#cli-to-schema-mapping] Most CLI flags are thin aliases over config schema fields. For example: ```bash npx @open-wa/wa-automate@5.1.0 \ --session-id sales \ --host 127.0.0.1 \ --port 8080 \ --api-key "$WA_API_KEY" \ --license-key "$WA_LICENSE_KEY" ``` is equivalent to these config fields: ```ts export default { sessionId: 'sales', port: 8080, apiKey: process.env.WA_API_KEY, licenseKey: process.env.WA_LICENSE_KEY, }; ``` Explicit CLI flags win over config-file and environment values. The published CLI `5.1.0` also applies its `0.0.0.0` and `8002` startup defaults after config loading, so always pass `--host` and `--port` when those values matter. Not every flag is a schema field. `--config` selects which file to load, `--pm2` controls process management, and `--name` controls the PM2 process name. ## Deprecated flags [#deprecated-flags] Some v4-era flags remain as compatibility warnings in v5. Replace them with the documented v5 settings below. | Flag or field | Status in v5 | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--ready-webhook` | Warns that ready-webhook delivery is not yet wired in the v5 CLI. | | `--stats` | Warns that swagger-stats parity is not restored. | | `--generate-api-docs` | Warns that runtime docs are always served from the shared API layer. | | `--tunnel` | Warns that tunnel setup parity is not yet restored in the v5 CLI. | | `--mcp` | The current source does not parse this flag. Configure `mcp: { enabled: true }` in `wa.config.*` and select the file with `--config ./wa.config.mjs` if needed. | | `--api-host` | Not parsed by the current v5 CLI. Use `--host`/`-h` for the local bind host and configure any public origin in the integration that needs it. | | `--chatwoot-url`, `--twilio-webhook`, `--bot-press-url` | Legacy integration flags that are not yet wired into the v5 CLI boot path. Prefer plugins and config files. | | `sessionData`, `sessionDataPath`, `skipSessionSave` | Deprecated MD-obsolete JSON session fields. Prefer `userDataDir` managed persistence. | | `popup`, `qrPopUpOnly`, `autoRefresh`, `qrRefreshS` | Legacy QR/popup behavior. Do not treat these as stable v5 runtime contracts. | When in doubt, prefer the config schema and the flags in this page over older v4 examples. ## Plugins field docs [#plugins-field-docs] Use `plugins` to load reusable integrations into the runtime process. ```ts export default { plugins: [ '@open-wa/integration-webhook', '@open-wa/integration-chatwoot', './plugins/my-local-plugin', '/absolute/path/to/plugin.js', ], }; ``` Each item is a plugin reference. It can be an npm package name, a scoped package name, or a file path. The module must export a default plugin or a named `plugin` export. Plugins loaded from `plugins` are imported during `createClient()` startup. If a plugin does not expose `meta.name`, it cannot be registered correctly. ## PluginConfig docs [#pluginconfig-docs] Use `pluginConfig` for plugin-specific settings. The object is keyed by each plugin's `meta.name`, not necessarily by the package name. ```ts export default { plugins: ['@open-wa/integration-webhook', './plugins/moderation'], pluginConfig: { webhook: { url: 'https://your-app.example/webhooks/open-wa', headers: { 'X-Webhook-Secret': process.env.WEBHOOK_SECRET, }, }, moderation: { enabled: true, apiKey: process.env.MODERATION_API_KEY, }, }, }; ``` At startup, open-wa looks up `pluginConfig[plugin.meta.name]`. If the plugin defines a config schema, that value is validated before it is passed to the plugin's `init()` function. ## Example with plugin [#example-with-plugin] ```ts import { defineConfig } from '@open-wa/config'; export default defineConfig({ sessionId: 'support', port: 8080, host: '0.0.0.0', apiKey: process.env.WA_API_KEY, webhook: process.env.WA_WEBHOOK, plugins: [ '@open-wa/integration-webhook', './plugins/internal-audit-plugin', ], pluginConfig: { webhook: { url: process.env.WA_WEBHOOK, headers: { 'X-Webhook-Secret': process.env.WEBHOOK_SECRET, }, }, 'internal-audit': { enabled: true, logLevel: 'info', }, }, }); ``` ## Secret management [#secret-management] Keep secrets out of committed config files. Use environment variables for `apiKey`, `licenseKey`, webhook secrets, Chatwoot tokens, and plugin API keys. For local development, keep the `.env` file in `.gitignore`. In production, use the platform secret store or a secret manager. Do not log complete config objects. Nested `pluginConfig` values frequently contain tokens. ## Session events worth knowing about [#session-events-worth-knowing-about] If you need QR images, session-data capture, or low-level launch state, read **[Session events](/docs/guides/session-events)**. ## One important caveat [#one-important-caveat] The old Docusaurus site generated a complete CLI options table at build time. This page gives the common flags first. The detailed config surface must agree with the generated reference and runtime schema. # Consume events with Effect (/docs/guides/effect-event-streams) # Consume events with Effect [#consume-events-with-effect] Use ordinary listeners for immediate application reactions. Add an Effect subscription when a separate consumer needs sequential work, batching, or automatic cleanup. Each PubSub subscriber receives the feed independently. These APIs are available from OpenWA 5.6.0. The examples use Effect 4, matching OpenWA's pinned `effect@4.0.2`. ## Run a callback and a queued consumer [#run-a-callback-and-a-queued-consumer] Install the standalone emitter and matching Effect version: ```sh npm install @open-wa/hyperemitter@5.6.0 effect@4.0.2 ``` Save this as `events.mjs`. It doesn't require a WhatsApp session: ```js import { EventEmitter } from '@open-wa/hyperemitter'; import { Effect, PubSub } from 'effect'; const events = new EventEmitter(); await Effect.runPromise( Effect.scoped(Effect.gen(function* () { yield* events.onScoped('app.job.finished', ({ jobId }) => { console.log('Callback:', jobId); }); const subscription = yield* events.subscribe(); events.emit('app.job.finished', { jobId: 'daily-summary' }); const { event, args } = yield* PubSub.take(subscription); console.log('Queued:', event, args[0].jobId); })).pipe(Effect.ensuring(events.close())), ); ``` Run `node events.mjs`. It prints: ```text Callback: daily-summary Queued: app.job.finished daily-summary ``` The callback runs inside `emit()`. The queued reader sees the same payload object through `args[0]`. Closing the consumer scope removes its subscription and scoped callback; the finalizer closes this application-owned emitter. ## Use OpenWA's shared feed [#use-openwas-shared-feed] Replace the standalone emitter with `ev` from `@open-wa/wa-automate` when observing sessions or sharing events across files. Acquire the subscription before starting `create()` if you need authentication events, because the default feed has no replay. Inside your application's Effect scope: ```ts import { ev } from '@open-wa/wa-automate'; import { Effect, PubSub } from 'effect'; const observeReady = Effect.gen(function* () { const subscription = yield* ev.subscribe(); while (true) { const frame = yield* PubSub.take(subscription); if (frame.event !== 'client.ready') continue; const [payload, sessionId, namespace] = frame.args; console.log('Ready:', sessionId, namespace, payload); } }); ``` Run this consumer as part of the scope that owns your application work. Global native frames carry all three callback arguments. Enabled session aliases are also forwarded, so filtering one naming scheme avoids processing a native event and its alias as two messages. The feed can include pairing and session data; select the fields your consumer actually needs. A subscription owns queued retention while active. End its scope when its consumer stops. Stopping one WhatsApp client detaches only that client's forwarding connection; don't close the shared `ev` as a consumer finalizer. ## Consume a typed stream [#consume-a-typed-stream] `client.events` has typed `{ event, payload }` frames. The standalone `HyperEmitter` uses the same interface, so this complete example can run without connecting a session. Save it as `typed-events.ts`: ```ts import { HyperEmitter } from '@open-wa/hyperemitter'; import { Effect, Stream } from 'effect'; type Events = { 'app.job.finished': { jobId: string }; 'app.status': { state: string }; }; const events = new HyperEmitter(); events.pubsub({ strategy: 'sliding', capacity: 16, replay: 2 }); events.emit('app.status', { state: 'ready' }); events.emit('app.job.finished', { jobId: 'daily-summary' }); await Effect.runPromise( events.stream().pipe( Stream.filter(frame => frame.event === 'app.job.finished'), Stream.take(1), Stream.runForEach(frame => Effect.sync(() => console.log('Stream:', frame.payload.jobId)), ), Effect.ensuring(events.close()), ), ); ``` Run it with your TypeScript runner, for example `bun run typed-events.ts`. It prints `Stream: daily-summary`. The two replay slots deliberately retain both emissions made before stream consumption. Without replay, a stream only sees emissions after its subscription begins. `Stream.runForEach()` processes the selected frames sequentially; add batching or concurrency explicitly when your consumer needs it. ## Choose retention for your consumer [#choose-retention-for-your-consumer] Configure the shared hub once, before other modules subscribe: ```ts ev.pubsub({ strategy: 'sliding', capacity: 256 }); ``` Use sliding for a monitor that prefers current observations over an old backlog. Use dropping when already-queued observations should take priority over new ones. Both policies can lose events. The unbounded default avoids capacity rejections but lets a slow active consumer retain a growing backlog. `ev.pubsubStatus` reports accepted and rejected emitter publications. An accepted frame hasn't necessarily been processed; sliding evictions aren't counted as rejections. Define acknowledgement in your own consumer when completion matters. An in-memory feed doesn't provide durable delivery across process restarts. ## Wait for callback work when needed [#wait-for-callback-work-when-needed] For an application operation that should await its direct listeners, use `emitAsync()` or `emitEffect()` rather than treating publication as completion: ```ts await ev.emitAsync('app.job.finished', { jobId: 'daily-summary' }); // Or, inside an Effect generator: // yield* ev.emitEffect('app.job.finished', { jobId: 'daily-summary' }); ``` These methods wait for direct callback work, including returned Promises or Effects. They don't wait for queued consumers. Register an Effect listener with `onScoped()` when its work should be interrupted as that scope closes; an external Promise operation needs its own cancellation support. Native access is available through `ev.pubsub()`. Publishing directly with `PubSub.publish()` reaches queued readers only. Use the emitter's dispatch methods when both callbacks and queued consumers should receive the event. See the [HyperEmitter reference](/docs/reference/hyperemitter) for exact method returns, buffer options, counters, and shutdown behavior. # Group vs DM Handling (/docs/guides/group-filtering) # Group vs DM Handling [#group-vs-dm-handling] Use `isGroupMsg` to distinguish group traffic from direct messages, then use the complete `chatId` value for routing. Do not rebuild a group ID from its name or assume every incoming identifier has the same format. See the [Chat ID guide](/docs/guides/chatid-primer) for the accepted input forms. ```ts client.onMessage(async (message) => { if (message.isGroupMsg) { console.log('Group message:', message.chatId); return; } console.log('Direct message:', message.chatId); }); ``` ## Allow or block groups [#allow-or-block-groups] Populate the set with group IDs copied from a lookup or message event. Keeping the exact returned value avoids assumptions about the identifier suffix. ```ts const allowedGroups = new Set([ '447123456789-1445627445@g.us', ]); client.onMessage(async (message) => { if (message.isGroupMsg && !allowedGroups.has(message.chatId)) return; await handleMessage(message); }); ``` For a blocklist, replace the condition with `message.isGroupMsg && blockedGroups.has(message.chatId)`. ## Sender and administrator rules [#sender-and-administrator-rules] In group messages, `author` identifies the participant when WhatsApp provides it. Compare full contact IDs, not a bare number stripped from the ID. The current embedded `Client` does not expose `getGroupAdmins`; the generated Easy API has a [`getGroupAdmins` method](/docs/reference/client/groups#getgroupadmins). If your workflow uses that API, compare its returned contact IDs with `message.author` and handle missing authors or lookup failures explicitly. ```ts const adminsByGroup = new Map>(); client.onMessage(async (message) => { if (!message.isGroupMsg || !message.author) return; const adminIds = adminsByGroup.get(message.chatId); if (adminIds?.has(message.author)) return; await moderate(message); }); ``` This example uses application-owned admin data. Refresh it from the supported Easy API method when permissions change; do not treat a cached list as current membership state. See the [groups guide](/docs/guides/groups) for method availability and the [event reference](/docs/reference/events) for group membership events. # Groups (/docs/guides/groups) # Groups [#groups] Group IDs are assigned by WhatsApp. Keep the complete ID returned by a method or message event; do not derive it from a group name or participant numbers. See the [Chat ID guide](/docs/guides/chatid-primer) for accepted identifier forms and invite-token handling. ## Create a group [#create-a-group] `createGroup` requires a Restricted license. It returns a group object with a `gid`, or `null` if creation did not return a group. The connected account must also be able to create the group and add the listed contacts. ```ts const created = await client.createGroup('Support updates', [ '447700900000@c.us', ]); if (!created?.gid) { throw new Error('The group was not created'); } const groupId = created.gid; ``` See the exact [`createGroup` contract](/docs/reference/client/client#creategroup) and [license guidance](/docs/licensing/pricing) before using this method. ## Participants and permissions [#participants-and-permissions] The embedded `Client` supports adding, removing, promoting, and demoting one participant at a time. These operations require group-administrator permissions, and each call returns `true` only when the runtime reports success. ```ts const changed = await client.addParticipant(groupId, '447700900000@c.us'); if (!changed) { console.error('The participant was not added'); } ``` Use the [participant method reference](/docs/reference/client/groups#addparticipant) for the other operations and their exact signatures. Keep the returned contact IDs intact; see [group filtering](/docs/guides/group-filtering) for matching incoming messages to a group allowlist. ## Invite links [#invite-links] `getGroupInviteLink` and `revokeGroupInviteLink` operate on a group ID. The returned invite URL is a token for joining, not the group ID. Joining through a link requires a Restricted license. The embedded `Client` returns a group ID or `false`; the Easy API also accepts an option to return a chat object. ```ts const inviteUrl = await client.getGroupInviteLink(groupId); const result = await client.joinGroupViaLink(inviteUrl); if (typeof result === 'string') { console.log('Joined group:', result); } else { console.error('The invite did not return a group ID:', result); } ``` The [`joinGroupViaLink` contract](/docs/reference/client/client#joingroupvialink) includes its license tier and Easy API option. Check the result for your selected surface before treating the join as complete. Revoking an invite invalidates the old URL; retrieve and share the replacement only when your workflow needs one. ## Other group operations [#other-group-operations] The generated Easy API catalog includes group reads and settings updates such as `getGroupMembers`, `getGroupAdmins`, and `setGroupDescription`. Method availability differs by client surface: the current embedded `Client` does not declare every Easy API group method, and its `getGroupInfo`, `setGroupTitle`, and `setGroupDescription` entries are unsupported. Check the exact method reference for your deployed surface before calling one: * [Group methods](/docs/reference/client/groups) * [Licensed features](/docs/licensing/licensed-features) ## Membership events [#membership-events] The runtime event map exposes `group.participants.changed.global`. Its `change` payload contains the group ID, action, affected participant IDs, and the actor when available: ```ts import { ev } from '@open-wa/wa-automate'; ev.on('group.participants.changed.global', ({ change }) => { console.log(change.groupId, change.action, change.participantIds, change.by); }); ``` The event is a change notification, not a complete membership snapshot. Keep an application-owned group record if later actions depend on current state. See the [event reference](/docs/reference/events) for its full type. # Integrations Overview (/docs/guides/integrations-overview) # Integrations Overview [#integrations-overview] open-wa provides several ways to connect WhatsApp to other services. This guide helps you choose the right pattern for your use case. ## Decision Tree [#decision-tree] ### Start Here [#start-here] Ask yourself: 1. **Do you want to push events to an existing service?** Use webhooks 2. **Do you want a full inbox experience?** Use Chatwoot or a CRM integration 3. **Do you want to build custom logic?** Use plugins or SocketClient 4. **Do you want AI agents to control WhatsApp?** Use MCP ### Integration Patterns [#integration-patterns] | Pattern | Best for | Complexity | | ---------------- | ------------------------------ | ---------- | | Webhooks | Pushing events to your service | Low | | SocketClient | Building a bot in Node.js | Medium | | Plugins | Reusable integrations | Medium | | Chatwoot | Full inbox with agents | Medium | | MCP | AI agent control | Low | | Embedded runtime | Full control over browser | High | ## Data Flow: WhatsApp to Your Service [#data-flow-whatsapp-to-your-service] ### Webhook Flow [#webhook-flow] ``` WhatsApp → open-wa → HTTP POST → Your Service ``` Messages and events are pushed to your endpoint as JSON payloads. ### Plugin Flow [#plugin-flow] ``` WhatsApp → open-wa → Plugin → External API → Response ``` Plugins run inside the open-wa process and can call external services directly. ### Chatwoot Flow [#chatwoot-flow] ``` WhatsApp → open-wa → Chatwoot API → Agent Inbox Agent Reply → Chatwoot API → open-wa → WhatsApp ``` Bidirectional sync between WhatsApp and Chatwoot. ## Contact and Message Sync [#contact-and-message-sync] ### How Contacts Work [#how-contacts-work] When a message arrives, open-wa has access to: * The sender's phone number (in `message.from`) * The sender's display name (if available) * Group membership information ### Message Synchronization [#message-synchronization] * **Incoming messages**: Delivered to your integration in real-time * **Outgoing messages**: Sent through your integration's API * **Message status**: Delivery receipts, read receipts, and typing indicators ## Security and Compliance [#security-and-compliance] ### Data Storage [#data-storage] * open-wa stores session data locally (authentication tokens, browser state) * Messages are not stored by open-wa unless your integration saves them * Media is decrypted on demand and not persisted ### GDPR Considerations [#gdpr-considerations] * You are responsible for handling personal data in messages * Consider implementing message retention policies * Document what data your integration stores and why ### Data Retention [#data-retention] Implement retention policies in your integration: * Delete old messages after a set period * Allow users to request data deletion * Log only what is necessary for operation ## Licensing for Integrations [#licensing-for-integrations] Licensing follows the methods and runtime behavior that an integration uses, not only its wrapper name. Each integration can call different parts of the API surface. Examine the [pricing page](/docs/licensing/pricing) and method reference before you promise a workflow to a customer. ## Choosing Your Path [#choosing-your-path] ### For Quick Integrations [#for-quick-integrations] Start with webhooks: 1. Add `@open-wa/integration-webhook` to `plugins` in `wa.config.*` 2. Point `pluginConfig.webhook.url` at your service endpoint 3. Handle incoming JSON payloads ### For Custom Logic [#for-custom-logic] Use plugins: 1. Install the plugin SDK 2. Build your integration as a plugin 3. Load it via `wa.config.js` ### For Full Inbox [#for-full-inbox] Use Chatwoot: 1. Set up Chatwoot 2. Configure the Chatwoot plugin 3. Connect your WhatsApp session ### For AI Agents [#for-ai-agents] Use MCP: 1. Enable MCP in config with `mcp: { enabled: true }` and keep `apiKey` configured 2. Verify whether your installed build mounts the configured `/mcp` route 3. If `/mcp` is mounted, point your AI client at that endpoint so it can discover and call WhatsApp methods 4. If your build does not mount `/mcp`, give the agent the HTTP API docs or OpenAPI schema. You can also give it the Postman collection. ## Related [#related] * **[Webhooks for business](/docs/guides/webhooks-for-business)** - Webhook setup and payloads * **[MCP Integration](/docs/guides/mcp)** - AI agent integration * **[Chatwoot integration](/docs/client-and-integrations/chatwoot)** - Full inbox setup * **[Plugin getting started](/docs/plugins/getting-started)** - Build custom integrations # Interactive messages (/docs/guides/interactive-messages) # Interactive messages [#interactive-messages] Interactive messages require an applied **Insiders or higher** license. Use one JSON document with the SDK, HTTP API or a message builder. `sendInteractive` returns the serialized outgoing message ID; that confirms the send operation, not recipient rendering or interaction. ```ts import { defineInteractiveMessage } from '@open-wa/wa-automate'; const message = defineInteractiveMessage({ type: 'buttons', body: 'How can we help?', actions: [ { type: 'reply', id: 'sales', label: 'Talk to sales' }, { type: 'reply', id: 'support', label: 'Get support' }, ], }); client.onInteractiveResponse(response => { if (response.type === 'reply') console.log(response.id, response.originalMessageId); }); const messageId = await client.sendInteractive('PHONE_NUMBER@c.us', message); ``` The HTTP equivalent is `POST /api/messages/sendInteractive` with `{ "to": "PHONE_NUMBER@c.us", "content": message }`. Supply your configured API authentication. SDK and HTTP calls use the same validation and client implementation. Illustration of quick reply buttons with stable response IDs *Illustrated examples: recipient appearance varies by WhatsApp version. All interactive message types require Insiders or above.* ## Connect the SDK client [#connect-the-sdk-client] The examples use the `Client` facade. If you are starting a standalone SDK app, create the runtime, attach the facade and start the session: ```ts import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; const runtime = await createClient({ driver: new PuppeteerDriver(), licenseKey: process.env.WA_LICENSE_KEY, headless: false, }); const client = new Client({ client: runtime, transport: runtime.getTransport() }); client.onFormResponse(response => console.log(response.answers)); await client.start(); // Authenticate in the browser, then use client.sendInteractive(...). // When your application shuts down: await client.stop(); ``` `createClient` from the core runtime exposes session lifecycle; the `Client` facade supplies messaging methods and typed listeners. The Easy API constructs this facade for you. ## Document contract [#document-contract] `version: 1` is optional and describes the OpenWA document format. Additive content types can be introduced without changing existing documents. Unknown fields are rejected so misspelled properties do not silently disappear. `InteractiveContentSchema` and the individual content/action schemas are exported by `@open-wa/schema`; `InteractiveContentSchema`, `defineInteractiveMessage` and the content types are also exported by the main package. Builders should store this document and export it directly, with no separate builder-specific wire format. Every content variant has `type`, `body` and optional `footer`. Use application-defined stable IDs for replies, list rows, questions and options. Labels can change without changing how your application identifies the response. Lists default to “Choose an option” and bookings to “View booking” when `buttonLabel` is omitted. `defineInteractiveMessage` fills those defaults and retains the specific content type, so a list remains a list in TypeScript autocomplete. | Type | Content | | ---------- | --------------------------------------------------------------------------- | | `buttons` | `actions`, optional `header` | | `list` | `sections`, optional `buttonLabel` and `title` | | `form` | `questions`, optional `title` | | `carousel` | `cards`, each with an image `header`, `body` and `actions` | | `booking` | `booking`, optional `buttonLabel`, `title` and additional CTA `actions` | | `native` | Native `actions` and optional `parameters`/`header` for advanced extensions | ### Actions and headers [#actions-and-headers] Illustration of separate URL, call and copy actions *Offer a website link, phone action or copyable code. Keep these separate from reply buttons.* ```ts { type: 'reply', id: 'yes', label: 'Yes' } { type: 'url', label: 'Open website', url: 'https://openwa.dev' } { type: 'call', label: 'Call us', phoneNumber: '+441234567890' } { type: 'copy', label: 'Copy code', code: 'WELCOME' } ``` Use reply actions together, or URL/call/copy actions together. Mixing reply and CTA actions is rejected. A URL action can open a map URL; sending a location card is a separate operation. Opening a URL, placing a call or copying text does not produce a promised response event. Buttons and native messages accept text, image, video or document headers: ```ts { type: 'text', text: 'Welcome' } { type: 'image', source: 'https://example.com/photo.jpg', title: 'A photo' } { type: 'document', source: '/absolute/path/brochure.pdf', filename: 'brochure.pdf' } ``` Media sources can be HTTP(S) URLs, file paths on the API host, or base64 data URLs. They are resolved by the client and uploaded before sending. A saved website-builder document should use URLs or data URLs rather than paths on the user's computer. Recipient/version support varies; video/document interactive headers still require runtime acceptance for your recipient. Booking content deliberately has no image/header option. ### Lists [#lists] Illustration of a list message and its sectioned option picker *Group a longer menu into sections and handle the selected row ID.* ```ts await client.sendInteractive(to, { type: 'list', body: 'Choose a service', buttonLabel: 'View services', sections: [{ title: 'Help', rows: [ { id: 'install', title: 'Installation', description: 'Get started' }, { id: 'account', title: 'Account help' }, ] }], }); ``` Row IDs must be unique across sections. A selected row arrives as `type: 'list'` with `id` and optional `label`. ### Forms [#forms] Illustration of a form with a single-choice question *Ask for one choice with singleSelect.* Illustration of a form with several checked options *Collect several selections with multiSelect.* ```ts const subscription = client.onFormResponse(response => { console.log(response.messageId, response.originalMessageId, response.answers); // { size: 'small', extras: ['delivery', 'gift'] } }); await client.sendInteractive(to, { type: 'form', title: 'Your preferences', body: 'Tell us what you need', questions: [ { type: 'singleSelect', id: 'size', label: 'Choose a size', options: [ { id: 'small', label: 'Small' }, { id: 'large', label: 'Large' }, ] }, { type: 'multiSelect', id: 'extras', label: 'Choose extras', options: [ { id: 'delivery', label: 'Delivery' }, { id: 'gift', label: 'Gift wrapping' }, ] }, ], }); // Stop this subscription later: subscription.off(); ``` Question IDs are unique within the form, and option IDs within each question. `allowCustomAnswer` is optional; applications should handle returned values beyond predefined option IDs when enabling it. Selection-count/required-field rules beyond the exposed choices are not implied by labels such as “choose two”. Illustration of a completed multi-question form and structured response object *Question IDs connect the choices the recipient sees to the answers your code receives.* `onFormResponse` filters the same underlying stream as `onInteractiveResponse`; registering both deliberately delivers a form submission to both handlers. Responses carry `messageId`, `chatId`, `senderId`, optional `originalMessageId`/`timestamp`, and `raw`. `originalMessageId` may be a stanza ID rather than a full serialized key. Unexpected native response formats remain available as `type: 'native'` with `name`, `parameters` and an optional `parseError`. HTTP integrations can subscribe to the same responses before sending a message: ```sh curl -N -H "X-API-Key: $WA_API_KEY" \ 'http://localhost:8002/api/events?topics=interactive.response' ``` Each `interactive.response` event contains the response at `payload.response`. Use its `type` to select forms, replies or list selections, and its `messageId` to deduplicate events in your integration. The stream does not retain events for disconnected integrations. WhatsApp can deliver older responses during session synchronization; deduplicate by `messageId` and use `timestamp` when your workflow needs to reject stale submissions. ### Carousels [#carousels] Illustration of two image cards with distinct reply actions *Each card has an image and its own actions.* ```ts await client.sendInteractive(to, { type: 'carousel', body: 'Choose a package', cards: [ { body: 'Starter', header: { type: 'image', source: starterImage }, actions: [{ type: 'reply', id: 'starter', label: 'Choose Starter' }] }, { body: 'Pro', header: { type: 'image', source: proImage }, actions: [{ type: 'reply', id: 'pro', label: 'Choose Pro' }] }, ], }); ``` Card images are uploaded without sending separate seed messages. Give reply actions distinct IDs across cards if you need to distinguish the card from the response alone. Action combinations and recipient limits are subject to WhatsApp support. ### Bookings [#bookings] Illustration of a booking card and appointment details *The recipient app supplies the calendar icon and details layout.* ```ts await client.sendInteractive(to, { type: 'booking', title: 'Consultation', body: 'Your booking details', buttonLabel: 'View booking', booking: { startAt: '2026-10-15T10:00:00+01:00', endAt: '2026-10-15T10:30:00+01:00', location: 'London', managementUrl: 'https://example.com/bookings/123', description: 'Please arrive five minutes early.', }, actions: [{ type: 'url', label: 'Our website', url: 'https://example.com' }], }); ``` The shared Effect codec validates booking dates and normalizes offsets and fractional seconds to UTC whole seconds, so Android renders the date and time in the native details panel. For example, `2026-07-26T14:00:00.000+01:00` becomes `2026-07-26T13:00:00Z`. This happens automatically in `defineInteractiveMessage`, the SDK, HTTP API and OpenUI; don't format dates separately. Invalid calendar dates and an `endAt` that isn't after `startAt` at whole-second precision are rejected. Parsed booking dates have the branded TypeScript type `BookingTimestamp`, so unchecked strings can't be assigned to `DecodedInteractiveContent`. Use `normalizeBookingTimestamp(value)` when constructing a typed timestamp yourself. At least one of `location`, `url` or `phoneNumber` is required. Optional booking fields include `url`, `phoneNumber`, `email` and `labels` (`language`, `meetingType`, `detailsTitle`, `addToCalendar`, `viewOnMap`, `manageBooking`). The recipient app controls the details UI and icon; custom logos/header images are not supported by this contract. ## Advanced and future payloads [#advanced-and-future-payloads] `type: 'native'` takes `{ name, parameters? }` actions and optional message `parameters`. Objects are serialized by the implementation; callers do not need to double-encode JSON. This is an explicit advanced extension: passing schema validation does not establish that a new action is accepted by WhatsApp, enabled for the account or rendered by the recipient. `client.sendRawMessage(to, payload)` accepts a JSON-compatible constructed protobuf **Message** object, including its message context. Its HTTP route is `POST /api/messages/sendRaw` with `{ to, payload }`. The licensed `B` function uses the same sender. WhatsApp prepares the local message model and handles encryption and fanout; the supplied protocol payload is retained during that send. It is not the higher-level Baileys sendMessage convenience format and does not upload media referenced inside arbitrary payloads. Byte fields use base64 for JSON transport. Pass the message directly, such as `{ interactiveMessage: ... }`; view-once and ephemeral wrappers are currently unsupported. Unknown fields, protobuf map fields and payloads the current WhatsApp parser cannot prepare are rejected. Arbitrary raw payload fidelity across later retries or a session restart is not guaranteed; use the typed content variants for routine messages. No automatic retry is added around a failed send because an ambiguous result may already have reached the recipient. `defineInteractiveMessage` validates a document only; it does not check recipient capability, resolve media or send it. Validation errors identify the invalid JSON field. Media-loading errors identify the header or carousel card, for example `content.cards[1].header.source`. Send errors expose the failure message directly; SDK errors retain the browser stack in `cause` for diagnosis. # Jev chat assistant (/docs/guides/jev) # Jev chat assistant [#jev-chat-assistant] Use `@open-wa/integration-jev` for a text assistant in ordinary WhatsApp chats. Jev chooses the next action, Mistral composes the wording, and open-wa sends the message. It supports text, labelled confirmations, buttons, lists, forms and native booking cards. ## Create a project [#create-a-project] ```bash npm init @open-wa@latest chat-assistant -- --plugins jev cd chat-assistant ``` Choose **Easy API** for Neo, or **SDK** to run your own code, then JavaScript or TypeScript. The [project builder](/new?plugins=jev) offers the same choices. Copy `.env.example` to `.env` and fill in: ```dotenv MISTRAL_API_KEY=YOUR_MISTRAL_API_KEY JEV_API_KEY=YOUR_JEV_API_KEY ``` Run `npm start`. Your generated `wa.config.js` or `wa.config.ts` is discovered automatically, and the configured plugin is installed when needed. Pair WhatsApp, then send a text message from another account. ## Configure the assistant [#configure-the-assistant] For an existing project, add the plugin and optional settings: ```js plugins: ['@open-wa/integration-jev'], hooks: { chatFilter: { allow: ['447123456789@c.us'] }, }, pluginConfig: { jev: { agent: { instructions: [ 'Help people arrange demo appointments.', 'Send requested confirmations and choices in this chat.', ], }, conversation: { timeZone: 'Europe/London' }, }, }, ``` Instructions accept a string or an array. `conversation.timeZone` sets the agreed timezone; without it, the conversation must supply one. The answer model defaults to `mistral-small-latest`, and the classifier to `jev-latest`. Replace the example chat ID with the chats the assistant may operate in. `hooks.chatFilter` applies before every plugin receives chat events and before plugin actions run, including calls and interactive replies. Include the known phone-number ID and LID explicitly when the same person uses both. An empty `allow` array blocks every chat; omitting `chatFilter` leaves chats unrestricted. In Neo, **Plugins → Jev** edits these settings. The normal **Chat** view shows pause controls, OpenUI previews, action outcomes and expandable decision evidence. Keys stay on the host. SDK mode uses the same engine without Neo. ## Add voice [#add-voice] Select both **Jev** and **Voxtral** in the wizard, or configure both packages: ```js plugins: ['@open-wa/integration-jev', '@open-wa/integration-voxtral'], pluginConfig: { voxtral: { mode: 'both' } }, ``` Jev handles plain text; Voxtral handles voice notes and calls. They share recent context, submitted booking cards and send outcomes. Button and form replies during a call are acknowledged through its audio. Otherwise, a selection returns to the adapter that sent the control. Typed messages wait while the same person's call is active. See the [Voxtral guide](/docs/guides/voxtral) for voice settings and call licensing. Voxtral already installs the shared engine as a dependency. Add Jev to `plugins` when you also want ordinary text conversations to trigger the assistant. ## Corrections and dates [#corrections-and-dates] “No, I said 2 pm” can correct the time on the latest booking card while keeping its date, duration and location. The host restricts the correction to the fields Jev selected, copies recognizable literal values from recorded source spans, and calculates timestamps itself. Missing or ambiguous facts prompt a question. “Tomorrow” uses the captured turn instant in the agreed timezone. A bare weekday means its next occurrence on or after that day; “this Friday” means the current Monday-based week and “next Friday” the following week. A month/day without a year means its next occurrence. Invalid dates and ambiguous DST times require clarification. A wrong transcription can still supply the wrong source value. These actions change chat messages, not an external booking system. Interactive formats need their licensed capability; text and formatted text confirmations remain available without it. Unknown sends aren't retried automatically. Settings and the send journal persist locally; recent conversation context expires in memory and clears on restart. # Logging and audit trails (/docs/guides/logging-and-audit) # Logging and audit trails [#logging-and-audit-trails] open-wa plugin logs go through the host logger. Your application can also create its own Winston logger for selected events, but that second logger does not automatically capture the host's stdout or plugin logs. Decide which stream owns each record before you choose a retention or audit process. ## Host and plugin logs [#host-and-plugin-logs] Plugins receive a scoped logger: ```ts init: async ({ logger }) => { logger.info('Plugin initialized', { version: '1.0.0' }); logger.warn('API key is missing'); logger.error('Failed to connect', { code: 'ECONNREFUSED' }); logger.debug('Processing message', { messageId: 'redacted-id' }); } ``` By default those records go to the Easy API process's stdout/stderr. Capture that stream with your process supervisor or shell: The explicit flags below start a local `logs` session and bind the API to loopback. Set the session ID and host to the values your application uses. ```bash mkdir -p ./logs npx @open-wa/wa-automate@5.1.0 --session-id logs --host 127.0.0.1 --port 8080 \ >> ./logs/open-wa.log 2>&1 ``` The host's output format and persistence are deployment choices. Redirecting stdout does not capture a separately configured application logger, and creating a Winston logger does not change how host logs are stored. ## Application-owned rotating logs [#application-owned-rotating-logs] Install both the logger and the transport used by the sample: ```bash npm install winston winston-daily-rotate-file export OPENWA_SESSION_ID='sales' ``` ```ts import winston from 'winston'; import DailyRotateFile from 'winston-daily-rotate-file'; const rotate = new DailyRotateFile({ filename: './logs/open-wa-%DATE%.log', datePattern: 'YYYY-MM-DD', maxFiles: '14d', maxSize: '50m', zippedArchive: true, }); const sessionId = process.env.OPENWA_SESSION_ID; if (!sessionId) throw new Error('Set OPENWA_SESSION_ID to the Easy API session ID'); rotate.on('error', (error) => { // Surface a transport failure to the process supervisor; do not silently lose it. console.error('rotating log transport failed', error); }); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [rotate], }); logger.on('error', (error) => { console.error('application logger failed', error); }); export function recordMessage(message) { logger.info('message.received', { messageId: message.id, sessionId, chatId: message.chatId, timestamp: new Date().toISOString(), }); } ``` Pass `recordMessage` to your `Client` instance's `onMessage` listener when you want this record for each incoming message. The `winston-daily-rotate-file` import is required because `DailyRotateFile` is not provided by `winston` alone. The example records identifiers and routing fields; add message content only when the business purpose and access policy require it. ## Structured output and queries [#structured-output-and-queries] When your logger writes JSON, filter the file with `jq`: ```bash jq 'select(.message == "message.received")' ./logs/open-wa-*.log ``` If the command reports no matches, inspect the file path and logger level before changing application code: ```bash ls -lh ./logs tail -n 50 ./logs/open-wa-*.log ``` ## Store a narrow audit record [#store-a-narrow-audit-record] For a small app-owned journal, SQLite can store identifiers and timestamps without copying message bodies. Install `better-sqlite3`, create the table once, then insert only the fields your workflow needs: ```ts import Database from 'better-sqlite3'; const db = new Database('./logs/audit.sqlite'); db.exec(` CREATE TABLE IF NOT EXISTS message_audit ( session_id TEXT NOT NULL, message_id TEXT NOT NULL, chat_id TEXT NOT NULL, observed_at INTEGER NOT NULL, PRIMARY KEY (session_id, message_id) ) `); const insertAudit = db.prepare(` INSERT OR IGNORE INTO message_audit (session_id, message_id, chat_id, observed_at) VALUES (?, ?, ?, ?) `); function recordMessage(message) { insertAudit.run(sessionId, message.id, message.chatId, Date.now()); } ``` Install the database package alongside the logger dependencies with `npm install better-sqlite3`. This is an application record, not proof of regulatory compliance. Define who can read or delete it, how long it is retained, and whether your policy requires tamper evidence, legal hold, or a specific storage region. Keep tokens, QR values, session credentials, and complete message bodies out of logs unless a documented purpose requires them. ## Troubleshooting [#troubleshooting] | Symptom | Command or observation | Expected result | Next action | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | No host logs appear | `mkdir -p ./logs && npx @open-wa/wa-automate@5.1.0 --session-id logs --host 127.0.0.1 --port 8080 >> ./logs/open-wa.log 2>&1` | Startup lines appear in `./logs/open-wa.log`. | Check the working directory, permissions, and process-supervisor stdout capture; run the process in the foreground when you need to see stderr directly. | | Host log exists but rotating file is empty | `ls -lh ./logs; tail -n 50 ./logs/open-wa-*.log` | The application logger's file exists and contains JSON records after `recordMessage` runs. | Confirm `winston-daily-rotate-file` is installed, the import is present, and the app logger is actually called; host logs use a separate stream. | | Transport emits an error | Inspect stderr for `rotating log transport failed` | The error includes the path or filesystem cause. | Fix directory ownership, free disk space, or change the path, then restart the process. | | A webhook is retried | Inspect the webhook sender log for `Webhook delivery failed` | The line includes an attempt count and delay. | Make the receiver return a fast `2xx`; use its durable journal if restart replay is required. | | Audit rows exist but cannot support a policy claim | Review the stored columns and retention job | Rows contain identifiers and timestamps, but no automatic tamper or deletion controls. | Add the deployment's access, retention, deletion, regional, and evidence controls before calling the process compliant. | ## Retention is an explicit policy [#retention-is-an-explicit-policy] A retention job should be scoped to the fields and policy your application owns: ```ts const cutoff = Date.now() - 90 * 24 * 60 * 60 * 1000; const result = db.prepare('DELETE FROM message_audit WHERE observed_at < ?').run(cutoff); console.info('expired audit rows removed', result.changes); ``` This continues the SQLite example above. Ninety days is an example, not a legal requirement; choose a period that matches your data inventory, customer commitments, deletion requests, incident response, and provider lifecycle controls. ## Related [#related] * [Node-RED integration](/docs/guides/node-red) * [S3 media storage](/docs/guides/s3-media) * [Security and deployment](/docs/operations/security-and-deployment) * [Webhook payloads](/docs/client-and-integrations/webhook-payloads) # MCP integration (/docs/guides/mcp) # MCP integration [#mcp-integration] Use the **Model Context Protocol (MCP)** integration only when you want an authenticated AI agent to call Easy API tools. Treat the MCP endpoint like API access to your WhatsApp session. The current v5 runtime mounts the MCP adapter when `mcp.enabled` is true. Startup stops if this configuration has no Easy API `apiKey`. The CLI does not parse `--mcp`. Enable MCP through `wa.config.*`. When enabled, MCP can read and send WhatsApp data through the connected session. Keep it behind an API key, do not expose it publicly, and use a dedicated session for agent work when possible. ## Security checklist before you enable MCP [#security-checklist-before-you-enable-mcp] * Start Easy API with `apiKey`. Source validation refuses MCP configuration without one. * Send that key on every MCP request with `X-API-Key`, `api_key`, or `key` headers. * Share the MCP URL only with clients you trust to read and send WhatsApp messages. * Put the MCP endpoint behind HTTPS or a trusted reverse proxy before remote use. * Use a dedicated WhatsApp session for AI-agent experiments. * Watch logs and the MCP dashboard while testing new prompts or tools. ## What MCP gives the agent [#what-mcp-gives-the-agent] The Model Context Protocol is a standard for connecting AI assistants to external tools and data sources. When enabled in config, open-wa mounts the MCP adapter and an authenticated AI agent can call Easy API methods as tools, including methods that can: * Read incoming messages * Send messages to any contact or group * Access contact information * Manage groups * Send media (images, files, locations) Your MCP client, prompt, and permissions model control when the agent can call these tools. ## MCP vs direct HTTP API [#mcp-vs-direct-http-api] | MCP | Direct HTTP API | | -------------------------------------------------------- | ------------------------------------------- | | Authenticated AI agents can list available tools | You construct API calls yourself | | Implemented tools are generated from the schema registry | You must read API docs and build requests | | The implemented adapter enforces session readiness | You must check session state yourself | | MCP adapter uses a single Streamable HTTP endpoint | Multiple endpoints for different operations | ## Quick start [#quick-start] Enable MCP in config, then start Easy API with an API key. The runtime mounts the MCP adapter at the configured path. Create `wa.config.mjs`: ```js export default { apiKey: process.env.WA_API_KEY, port: 8080, mcp: { enabled: true, path: '/mcp', exposeToolsMeta: true, }, }; ``` Start Easy API from the same directory: ```bash WA_API_KEY="your-secure-key" npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id mcp --host 127.0.0.1 --port 8080 ``` The configured MCP path is: ```text http://localhost:8080/mcp ``` If you set `mcp.path` to another value, use that path in the MCP client URL. The dashboard is served separately at `/dashboard/mcp` when the dashboard is enabled. Enable MCP in the [configuration file](/docs/guides/configuration-and-cli); the CLI does not parse `--mcp`. ### Expected enabled state [#expected-enabled-state] After startup, check `/health`: ```bash curl -s http://localhost:8080/health | jq '.capabilities.mcpEnabled, .capabilities.mcpAvailable, .capabilities.mcpPath' ``` Expected local result after config is loaded and an API key is present, illustrative local verification guidance: ```text true true "/mcp" ``` This output is an example, not captured output. If `mcpEnabled` is `false`, the runtime did not load the config or `mcp.enabled` is not set. If `mcpAvailable` is `false`, make sure that `apiKey` is configured. The health fields show the config and API key state. The source mounts enabled MCP during API route registration. When tool metadata is enabled, you can also inspect the generated tool catalog: ```bash curl -s http://localhost:8080/meta/mcp-tools.json | jq '.endpoint, .requiresApiKey, (.tools | length)' ``` Expected local result, illustrative local verification guidance: ```text "/mcp" true ``` If the tool count is `0`, confirm the session finished authentication and the schema registry loaded. Tool metadata is useful for inspecting what is exposed, but it is not a successful MCP client handshake. ## Streamable HTTP transport [#streamable-http-transport] The adapter uses the **Streamable HTTP** transport for MCP. This means: * **Single endpoint**, no separate SSE or messages paths. Everything goes through the configured MCP path. * **Session readiness**, tools block until the WhatsApp session is fully connected. An AI agent cannot send messages before the session is ready. * **Stateful connections**, the server maintains MCP session state across requests. For a protocol-level check, connect with an MCP client and call `tools/list`. A healthy endpoint lists open-wa tools from the Easy API method catalog. A raw `curl` JSON-RPC request or `/meta/mcp-tools.json` is not a complete MCP client handshake. The Streamable HTTP transport needs the standard client flow. ## Configure AI clients [#configure-ai-clients] Use these client snippets after you start Easy API with `mcp.enabled` and an `apiKey` in config. Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "open-wa": { "url": "http://localhost:8080/mcp", "headers": { "X-API-Key": "your-secure-key" } } } } ``` After you add this configuration, restart Claude Desktop. The open-wa tools appear in the available tools list. If they do not appear, examine the `X-API-Key` header and make sure that the config loaded. Then, try `tools/list` again before you let the agent call send-message tools. 1. Open Cursor settings 2. Navigate to the MCP servers section 3. Add a new MCP server with: * URL: `http://localhost:8080/mcp` * Headers: `X-API-Key: your-secure-key` 4. Test the connection 1. Open Windsurf settings 2. Add a new MCP server 3. Configure: * URL: `http://localhost:8080/mcp` * Include `X-API-Key` in headers with your API key value 4. Verify the connection If you are building a custom MCP client: 1. Connect to `http://localhost:8080/mcp` with the Streamable HTTP transport 2. Include `X-API-Key: your-secure-key` in every request, or use the accepted `api_key` or `key` header names 3. Use the `tools/list` method to discover available tools after a successful MCP handshake 4. Use `tools/call` to execute tools ## Security model [#security-model] **MCP is an Easy API-only integration.** It is not available through `createClient()`. The API key is mandatory, and current source validation refuses MCP configuration without one. ### API Key Enforcement [#api-key-enforcement] Every MCP request must include the same API key as the Easy API. The adapter accepts `X-API-Key`, `api_key`, or `key` headers and rejects requests without a valid key. ### What data AI agents can access [#what-data-ai-agents-can-access] The adapter projects every method in the HTTP method schema catalog into an MCP tool. An authenticated AI agent can call those registered tools, including methods that can: * Read all messages in all chats * Send messages to any contact or group * Access contact information * Manage groups (create, modify, delete) * Send media ### Permission scopes [#permission-scopes] There are **no per-tool permission scopes** in the current implementation. Any authenticated agent can call any tool in that catalog. Plan your deployment accordingly: * Use a dedicated session for AI agent use * Do not share the MCP endpoint publicly * Monitor AI agent activity through logs ### What happens if access is not controlled [#what-happens-if-access-is-not-controlled] If the MCP endpoint is exposed publicly, the API key is leaked, or untrusted agents are allowed to use the key: * Those agents can control your WhatsApp session * Messages can be read and sent by unauthorized parties * Session information can be accessed Always use the API key, keep it secret, and consider placing the endpoint behind a reverse proxy. ### Audit AI-agent actions [#audit-ai-agent-actions] When you use MCP, monitor the AI agent through: * The dashboard MCP page (`http://localhost:8080/dashboard/mcp`), where available * API access logs * Your reverse proxy access logs * The session event log ## Tool discovery [#tool-discovery] ### How tools are generated [#how-tools-are-generated] The adapter projects method definitions from the HTTP method schema registry. Each registered method becomes an MCP tool with: * A name matching the API method * A description from the schema * Typed parameters ### List available tools [#list-available-tools] Connect with a real MCP client and call `tools/list` to see all available tools. The response includes each tool's name, description, and parameter schema. ### Same methods as the HTTP API [#same-methods-as-the-http-api] The implemented MCP tools are generated from the same method catalog as the HTTP API. Use the HTTP API docs as the fallback if MCP handshake or `tools/list` is unavailable. ## Dashboard MCP page [#dashboard-mcp-page] When available in your build, open `http://localhost:8080/dashboard/mcp` to see: * **Connection status**, whether MCP is configured and healthy * **Configuration snippets**, copy-paste configuration for Claude Desktop, Cursor, and Windsurf * **Tool details**, list of generated tool metadata with descriptions ## Configuration reference [#configuration-reference] In your `wa.config.mjs`: ```js export default { apiKey: "your-secure-key", mcp: { enabled: true, path: "/mcp", exposeToolsMeta: true, }, }; ``` ### Fields [#fields] * **`mcp.enabled`**, enable or disable MCP configuration (default: `false`) * **`mcp.path`**, the configured URL path for the MCP endpoint (default: `"/mcp"`) * **`mcp.exposeToolsMeta`**, whether to expose tool metadata in the dashboard (default: `true`) The environment adapter accepts the complete MCP object as JSON through `WA_MCP`. It does not expose separate `WA_MCP_ENABLED` or `WA_MCP_PATH` settings. The CLI does not parse `--mcp`, so use the config file when you want readable nested fields: ```bash WA_MCP='{"enabled":true,"path":"/mcp","exposeToolsMeta":true}' ``` ## Troubleshooting [#troubleshooting] ## Without MCP: HTTP alternative [#without-mcp-http-alternative] If you prefer not to use MCP, use the HTTP API docs and schemas directly: * **Interactive docs:** `http://localhost:8080/api-docs/` * **OpenAPI schema:** `http://localhost:8080/meta/swagger.json` * **Postman collection:** `http://localhost:8080/meta/postman.json` ## LLM-readable docs [#llm-readable-docs] The docs site generates LLM-readable content at: * **`/llms.txt`**, index of all docs pages in LLM-readable format * **`/llms-full.txt`**, full docs content for LLM consumption * **`/llms.mdx/docs/$`**, per-page MDX for LLMs AI agents can use these endpoints to read the docs without browsing the site. ## Agent discovery endpoints [#agent-discovery-endpoints] The docs site also exposes machine-readable discovery documents so agents and tools can find the API and skill surface programmatically: * **`/.well-known/mcp/server-card.json`** is reserved for deployments that host MCP. The docs site does not host MCP and returns 404 for this path. * **`/.well-known/agent-skills.index.json`**, index of published Agent Skills for the open-wa packages. * **`/.well-known/api-catalog`**, catalog pointing at the OpenAPI/HTTP API descriptions. * **`/.well-known/oauth-protected-resource`** and **`/.well-known/oauth-authorization-server`**, OAuth discovery documents for clients that negotiate auth automatically. These describe the docs/agent surface. To actually call WhatsApp methods, an agent still connects to a running Easy API instance with a valid API key as described above. ## Related [#related] * **AI agent patterns**, architecture patterns for AI agents * **[Easy API quick start](/docs/getting-started/easy-api)**, start the API * **Security & deployment**, production security # Media (/docs/guides/media) # Media [#media] `sendFile(to, input, filename?, options?)` and `sendImage` use the same native attachment pipeline. Both accept images, videos, audio and documents as a local path, URL, data URL, base64 string or `Buffer`. The filename is optional; it is inferred from a recognized path/URL or the resolved MIME type. Captions and quoted-message IDs belong in `options`. ```ts await client.sendImage(chatId, './photo.jpg', 'photo.jpg', { caption: 'Hello' }); await client.sendFile(chatId, './clip.mp4', 'clip.mp4', { caption: 'Watch this' }); await client.sendFile(chatId, './report.pdf', 'report.pdf', { caption: 'Report' }); await client.sendAudio(chatId, './sound.mp3'); await client.sendPtt(chatId, './voice.ogg'); await client.sendVideoAsGif(chatId, './loop.mp4'); ``` ## Choose presentation [#choose-presentation] Import `MediaSendOptions` from `@open-wa/client` or `@open-wa/schema`. The discriminator selects the options that apply to that media type; unknown fields and invalid combinations are rejected. | `type` | Options | Behavior | | ----------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Omitted or `auto` | `caption`, `quotedMsgId`, `viewOnce` | Detect MIME. View once requires an image or video; detected audio rejects captions. | | `image` | `caption`, `quotedMsgId`, `viewOnce` | Require image media. | | `video` | `caption`, `quotedMsgId`, `viewOnce`, `isGif`, `videoNote` | Require video. GIF and circular notes require MP4. | | `audio` | `quotedMsgId`, `ptt`, `viewOnce` | Require audio. View once requires `ptt: true`. | | `document` | `caption`, `quotedMsgId` | Force document presentation, including for an image/video/audio file. | These methods prepare and upload the supplied media through WhatsApp Web; they do not transcode it or install a media-processing sidecar. WhatsApp's upload constraints and recipient support still apply. A returned message ID or boolean confirms submission, not delivery; observe `onAck` for later acknowledgements and inspect the chat before retrying an uncertain send. ## View once [#view-once] ```ts await client.sendFile(chatId, './photo.jpg', 'photo.jpg', { type: 'image', viewOnce: true, quotedMsgId: messageId, }); await client.sendFile(chatId, './clip.mp4', 'clip.mp4', { type: 'video', viewOnce: true, }); await client.sendPtt(chatId, './voice.ogg', { viewOnce: true }); ``` For the generic method, the voice-note equivalent is `{ type: 'audio', ptt: true, viewOnce: true }`. Documents and ordinary audio attachments cannot be view once. Quoting preserves the view-once flag. ## Circular video notes and GIFs [#circular-video-notes-and-gifs] ```ts await client.sendFile(chatId, './square-note.mp4', 'note.mp4', { type: 'video', videoNote: true, }); await client.sendFile(chatId, './square-note.mp4', 'note.mp4', { type: 'video', videoNote: true, viewOnce: true, }); await client.sendFile(chatId, './loop.mp4', 'loop.mp4', { type: 'video', isGif: true, caption: 'Looping clip', }); ``` Circular notes use WhatsApp's PTV message type. Supply an already prepared square MP4 with a positive duration of at most 60 seconds. This API does not crop or shorten it. Video notes have no caption and cannot also be GIFs. GIF-style messages carry MP4 video with a playback flag; GIFs cannot be view once. Audio voice notes should use suitable prepared audio such as Ogg Opus. ## Send media as a document [#send-media-as-a-document] ```ts await client.sendFile(chatId, './photo.jpg', 'original.jpg', { type: 'document', caption: 'Original file', quotedMsgId: messageId, }); ``` Document presentation retains the file MIME type and filename. Its caption is separate from the filename. ## Remote URL auth [#remote-url-auth] `sendFileFromUrl` accepts the same presentation options and a separate headers object for the download. The URL must return the media bytes. ```ts await client.sendFileFromUrl( chatId, 'https://files.example.com/invoices/123.pdf', 'invoice-123.pdf', { type: 'document', caption: 'Invoice 123' }, { Authorization: `Bearer ${process.env.FILE_API_TOKEN}` }, ); ``` ## Data URLs and MIME [#data-urls-and-mime] ```ts import { readFile } from 'node:fs/promises'; const buffer = await readFile('./photo.jpg'); const dataUrl = `data:image/jpeg;base64,${buffer.toString('base64')}`; await client.sendImage(chatId, dataUrl, 'photo.jpg', { viewOnce: true }); ``` Use the actual media MIME type. For `application/octet-stream`, the client uses a recognized filename extension or file signature to identify the media. An unknown file defaults to a document. Base64 increases transfer size by about one third, and decoding/uploading also consumes browser and host memory. ## Decrypt incoming media [#decrypt-incoming-media] For two-way voice-note conversations through the official integration, see [Voxtral voice assistant](/docs/guides/voxtral). ```ts import { decryptMedia } from '@open-wa/wa-automate'; client.onMessage(async (message) => { if (!message.mimetype) return; const mediaData = await decryptMedia(message); // persist, inspect, or re-send it from here }); ``` ## DecryptMedia output [#decryptmedia-output] The standalone `decryptMedia` helper from the decrypt package returns a Node.js `Buffer` containing the raw decrypted bytes: ```ts import { writeFile } from 'node:fs/promises'; import { decryptMedia } from '@open-wa/decrypt'; const buffer = await decryptMedia(message); await writeFile('./incoming-media', buffer); ``` The higher-level client method in the current client surface returns a data URL so it can be passed straight back into send helpers: ```ts const dataUrl = await client.decryptMedia(message); const base64 = dataUrl.split(',')[1] ?? ''; const buffer = Buffer.from(base64, 'base64'); ``` Use `message.mimetype` to choose the file extension or upload metadata. Decryption needs the media fields WhatsApp provides on the message, including the media URL, media key, type, MIME type, hash, and usually size. ## Storage example [#storage-example] To save decrypted media to disk, convert the decrypted output into a buffer and write it with the right extension: ```ts import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import mime from 'mime-types'; import { decryptMedia } from '@open-wa/decrypt'; client.onMessage(async (message) => { if (!message.mimetype) return; const buffer = await decryptMedia(message); const extension = mime.extension(message.mimetype) || 'bin'; const filename = `${message.id ?? Date.now()}.${extension}`; const outputPath = path.join('media', filename); await mkdir('media', { recursive: true }); await writeFile(outputPath, buffer); }); ``` For S3, upload the same bytes and keep the MIME type as object metadata: ```ts import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { decryptMedia } from '@open-wa/decrypt'; const s3 = new S3Client({ region: 'us-east-1' }); client.onMessage(async (message) => { if (!message.mimetype) return; const buffer = await decryptMedia(message); const key = `whatsapp/${message.id ?? Date.now()}`; await s3.send(new PutObjectCommand({ Bucket: process.env.MEDIA_BUCKET, Key: key, Body: buffer, ContentType: message.mimetype, })); }); ``` If you want this handled automatically, use the S3 media integration instead of writing upload logic in every message handler. ## Recovery helpers [#recovery-helpers] Some older helpers such as stale-media recovery or sticker-specific decryption can depend on the runtime surface you are targeting. Validate those helpers in your deployment before baking them into a production flow. ## Stale media recovery [#stale-media-recovery] Incoming media points to encrypted blobs on WhatsApp servers. Those URLs can expire or become unavailable, especially for older messages or messages that were not downloaded while the session was active. When media is stale, decryption usually fails during the download step because the encrypted blob can no longer be fetched. Recovery helpers can sometimes ask the runtime to refresh the media reference, but success depends on WhatsApp still making the media available to that session. For production flows, decrypt and store media when you receive it. If the media is necessary for compliance, support, or analytics, save it before you complete the workflow. Use disk, object storage, or the S3 integration. ## Runtime limitations [#runtime-limitations] Media handling depends on more than the open-wa method call. The browser driver, host memory, network access, Chrome feature support, and WhatsApp Web changes can all affect send and decrypt behavior. Known limitations to plan around: * remote URLs must be reachable from the runtime host, not just from your app server * data URLs keep the full file in memory as a string, so large files can pressure Node.js and the browser * video, GIF, sticker, and voice-note behavior is more runtime-sensitive than plain document sending * stale media is not always recoverable after WhatsApp removes or expires the encrypted blob * MIME type mismatches can make media send as a document, fail validation, or decrypt without a useful extension * headless browser differences can affect media capture, conversion, and upload flows Test the exact media types, sizes, and runtime driver you plan to deploy. Small local examples are not a guarantee that high-volume or large-file production traffic will behave the same way. # Deleting Messages (/docs/guides/message-deletion) # Deleting Messages [#deleting-messages] `deleteMessage` accepts a chat ID, one message ID or an array of message IDs, and an optional `onlyLocal` flag. It returns a boolean: `true` means the runtime completed its deletion jobs, while `false` means the chat could not be found. The current runtime also returns `true` when none of the supplied message IDs match a loaded message, so that value alone does not prove a message was removed. Handle rejected calls too. Even when a revoke operation completes, the return value cannot prove that every recipient has removed the message. ## Local deletion versus revoke for everyone [#local-deletion-versus-revoke-for-everyone] The default is `onlyLocal: false`. For messages sent by this session, the runtime requests revoke for everyone; for incoming messages, it deletes the local copy. Revoke can depend on message ownership, group permissions, message age, account state, and current WhatsApp rules. Set `onlyLocal: true` when the goal is to remove the message from this session's local view only. Local deletion does not remove a message from other devices or recipients. ```ts const removedLocally = await client.deleteMessage( '447700900000@c.us', 'false_447700900000@c.us_ABC123', true ); if (!removedLocally) { console.error('The local deletion was not completed'); } ``` For a provider-side request, use `false` or omit the third argument and handle `false` as a failed request: ```ts const revoked = await client.deleteMessage( '447700900000@c.us', 'false_447700900000@c.us_ABC123' ); if (!revoked) { console.error('The deletion request did not complete; check the chat and message IDs'); } ``` ## Use a supported message ID [#use-a-supported-message-id] The in-process `Client` declares `sendText` as `Promise`; the current runtime can also return status strings such as `Not a contact`. The Easy API schema also permits a boolean or an object containing `_serialized`. Only a value matching the message-ID shape is safe to pass to `deleteMessage`: ```ts type SendResult = | string | boolean | { _serialized: string; [key: string]: unknown }; function messageIdFrom(result: SendResult): string | null { const value = typeof result === 'string' ? result : typeof result === 'object' && result !== null ? result._serialized : null; return value && /^(true|false)_.+_.+$/.test(value) ? value : null; } const sent = await client.sendText( '447700900000@c.us', 'This message may be removed shortly.' ); const messageId = messageIdFrom(sent); if (!messageId) { throw new Error(`sendText did not return a usable message ID: ${String(sent)}`); } const removed = await client.deleteMessage( '447700900000@c.us', messageId, false ); if (!removed) { console.error('The provider rejected or could not complete deletion'); } ``` The message ID format shown here is the current schema's representative shape. Prefer the ID returned by the send call or event rather than constructing one. If your method surface returns a serialized object, use its `_serialized` value; never pass the whole object or a boolean to `deleteMessage`. ## Incoming messages and permissions [#incoming-messages-and-permissions] You can request deletion from an incoming-message handler, but the provider may allow only messages you sent or messages covered by the connected account's group permissions. Older messages can also fall outside the provider's current revoke window. None of those windows or permissions are guaranteed by the open-wa method, so keep the failure path explicit: ```ts client.onMessage(async (message) => { if (!message.body.includes('[remove]')) return; try { const removed = await client.deleteMessage( message.chatId, message.id, false ); if (!removed) { console.warn('Deletion was rejected or unavailable', { chatId: message.chatId, messageId: message.id, }); } } catch (error) { console.error('Deletion failed before completion', error); } }); ``` Deleting a message cannot undo a recipient seeing, copying, or storing it before the provider processes the request. If your policy requires immediate redaction, remove the local copy and stop downstream processing while the provider result is unresolved. ## Related [#related] * **[Client message reference](/docs/reference/client/messages#deletemessage)**: Exact parameters and output * **[Messages guide](/docs/guides/messages)**: Send messages and handle result unions * **[Groups guide](/docs/guides/groups)**: Group membership and administrator behavior # Messages and message events (/docs/guides/messages) # Messages and message events [#messages-and-message-events] Use this guide for the common message flows: receiving messages, sending replies, attaching media, and reacting to message metadata. For chat ID formats such as `447700000000@c.us`, group IDs, and LID IDs, read **[ChatId primer](/docs/guides/chatid-primer)** first. ## Receive messages [#receive-messages] ```ts client.onMessage((message) => { console.log(message.body); }); client.onAnyMessage((message) => { console.log(message.body); }); ``` Use `onMessage` for incoming traffic only. Use `onAnyMessage` when you need incoming and outgoing traffic. ## Message object shape [#message-object-shape] Incoming message objects include the message text, sender details, chat details, media flags, and quote metadata. WhatsApp can add extra fields, so treat the object as extensible. ```ts client.onMessage((message) => { console.log(message.id); console.log(message.from); console.log(message.body); console.log(message.type); }); ``` | Field | Description | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `id` | The message ID. Pass this to `reply`, `forwardMessages`, or lookup methods. | | `body` | Text content for text messages. For media messages, this can be empty. | | `type` | WhatsApp message type, such as `chat`, `image`, `video`, `album`, `document`, `ptt`, `location`, `buttons_response`, or `list_response`. | | `event` | Structured event details for an `event_creation` message. See [WhatsApp events and RSVPs](/docs/guides/whatsapp-events). | | `from` | The chat that sent the message. In direct chats this is the contact ID. In groups this is the group chat ID. | | `to` | The chat or account that received the message. | | `chatId` | The chat ID for the conversation. | | `sender` | Contact details for the sender. | | `senderId` | Sender contact ID when available. Useful in groups. | | `author` | The group participant that sent the message. Present on many group messages. | | `fromMe` | `true` when the current session sent the message. | | `self` | Direction marker, usually `in` or `out`. | | `timestamp` / `t` | Unix timestamp values from WhatsApp. | | `isGroupMsg` | `true` when the message came from a group. | | `isMedia` / `isMMS` | Media flags. Use these before downloading or processing attachments. | | `caption` | Caption for image, video, or document messages. | | `expectedImageCount` / `expectedVideoCount` | Media counts declared by an album container. They don't confirm that all children have arrived. GIF videos contribute to the video count. | | `parentMsgKey` / `associationType` | Associated media's parent message ID and association type. Album children use `MEDIA_ALBUM`. | | `isGif` | Whether a video message plays as a WhatsApp GIF. | | `mentionedJidList` | Contact IDs mentioned in the message. | | `ack` | Delivery state for outgoing messages. Read `onAck` for updates. | | `quotedMsg` / `quotedMsgObj` | Quoted message payload when WhatsApp makes it available. | | `isQuotedMsgAvailable` | Whether quote data can be read from the payload. | ## Send messages [#send-messages] ```ts await client.sendText(chatId, 'Hello'); await client.reply(chatId, 'Hello', messageId); ``` The minimum call is `sendText(chatId, text)`. The generated [sendText reference](/docs/reference/client/client#sendtext) lists an optional `options` value retained for request compatibility, but the current Client implementation ignores it. Use [`reply`](/docs/reference/client/client#reply) with a message ID for a quoted reply. For mentions, use the generated Easy API method [`sendTextWithMentions`](/docs/reference/client/client#sendtextwithmentions) with contact IDs such as `447700900000@c.us`; the current embedded Client does not declare that method. The in-process `Client` returns a validated serialized `MessageId` from `sendText`, or throws a typed `SendTextError`. A returned ID confirms submission, not delivery or reading. Subscribe to `onAck` before sending to observe later acknowledgements. Starting a chat with a new number requires an applied Restricted or Premium license; an existing chat is enough for this tutorial. An error with `outcome: 'unknown'` means the message may already have been sent. Check the conversation before retrying. open-wa never retries `sendText` automatically. Mention helpers are generated Easy API methods with their own parameter contract; they are not methods declared by the current embedded `Client` surface. When using that API surface, the call shape is: ```ts await client.sendTextWithMentions( groupId, 'Hello @447700000000', false, ['447700000000@c.us'] ); await client.sendReplyWithMentions( groupId, 'Hello @447700000000', messageId, false, ['447700000000@c.us'] ); ``` ## Formatting [#formatting] WhatsApp uses inline formatting markers in text messages. Send the markers as part of the message body. ````ts await client.sendText(chatId, '*bold*'); await client.sendText(chatId, '_italic_'); await client.sendText(chatId, '~strikethrough~'); await client.sendText(chatId, '```monospace```'); ```` You can combine formatting with normal text. ```ts await client.sendText(chatId, 'Order *confirmed* for _today_.'); ``` ## Attachments vs text [#attachments-vs-text] Text messages only need a chat ID and a string body. Attachment messages include a file payload, a filename, and often a caption. ```ts import { readFile } from 'node:fs/promises'; await client.sendText(chatId, 'Here is the receipt.'); const image = await readFile('./receipt.png'); const imageDataUrl = `data:image/png;base64,${image.toString('base64')}`; await client.sendImage( chatId, imageDataUrl, 'receipt.png', { caption: 'Receipt attached' } ); ``` Media methods accept a local path, URL, data URL, base64 string or `Buffer`. `sendFile` and `sendImage` both use the native attachment pipeline and detect image, video, audio or document presentation. Captions, quote IDs and typed presentation settings go in their options object. The embedded Client also provides `sendAudio`, `sendPtt` and `sendVideoAsGif`. See [Media](/docs/guides/media) for view-once images/videos/voice notes, circular video notes, document presentation and the allowed combinations. ## Albums [#albums] The next release adds album receiving and sending. An album arrives with `type: 'album'`, declared image/video counts, and an empty body. The container has no downloadable media; its images and videos are separate messages. Album children carry `associationType: 'MEDIA_ALBUM'` and a serialized `parentMsgKey` matching the container's ID. Captions belong to those children. ```ts client.onMessage(async (message) => { if (message.type === 'album') { console.log('Album:', message.id, message.expectedImageCount, message.expectedVideoCount); const loaded = await client.getAlbumMessages(message.id); if (loaded !== false) console.log('Loaded items:', loaded.length); } if (message.associationType === 'MEDIA_ALBUM' && message.parentMsgKey) { console.log('Album item:', message.parentMsgKey, message.caption); const dataUrl = await client.decryptMedia(message); // Process this image or video in your application. console.log('Downloaded bytes as a data URL:', dataUrl.length); } }); ``` `getAlbumMessages(albumId)` returns the currently loaded image/video children, including GIF videos, or `false` if the container isn't loaded or the ID isn't an album. Its array can be empty or incomplete when the parent arrives before its children. Process child events as they arrive, or call the lookup again later; the lookup doesn't wait for every declared item. Pass each child to `decryptMedia` or `downloadMedia`. Send an album from real media files: ```ts import { readFile } from 'node:fs/promises'; const encode = async (path: string, mime: string) => `data:${mime};base64,${(await readFile(path)).toString('base64')}`; const result = await client.sendAlbum(chatId, [ { file: await encode('./photo.jpg', 'image/jpeg'), filename: 'photo.jpg' }, { file: await encode('./clip.mp4', 'video/mp4'), filename: 'clip.mp4' }, { file: await encode('./loop.mp4', 'video/mp4'), filename: 'loop.mp4', isGif: true }, { file: await encode('./photo-2.jpg', 'image/jpeg'), filename: 'photo-2.jpg' }, ], 'One caption for the album'); console.log(result.albumId, result.messageIds); ``` Images and videos can be mixed. For a WhatsApp GIF, provide an MP4 video with `isGif: true`; this API doesn't convert GIF files to MP4. Documents and audio are rejected. WhatsApp controls album availability, its minimum item count and upload limits. Phone-number IDs (`number@c.us`) are resolved to LIDs using WhatsApp's contact mapping, querying it when needed. LID, group and channel IDs are used directly. The destination chat must already be loaded and allow sending. The optional fourth argument quotes a loaded message in that chat on the first album item; the caption is also attached to that first item. The method prepares every item before submitting the container. It returns the parent ID and submitted media IDs in input order. These IDs confirm submission, not delivery or recipient rendering. If submission fails after the parent is created, the error includes the parent ID and submitted child IDs when the browser can report them. A lost connection can leave the outcome unknown, and the album may be partially sent, so inspect the chat before retrying. The method doesn't retry automatically. ## Quoted replies [#quoted-replies] Use `reply` when you want the response to quote an existing message. Pass the destination chat, reply body, and original message ID. ```ts client.onMessage(async (message) => { if (message.body === '!status') { await client.reply(message.from, 'Still working on it.', message.id); } }); ``` For replies that also mention users, use the generated Easy API method `sendReplyWithMentions`. ```ts await client.sendReplyWithMentions( groupId, 'Thanks @447700000000', messageId, false, ['447700000000@c.us'] ); ``` ## Mentions [#mentions] Mentions work in group messages when the body contains `@` text and the mentioned contact IDs are passed to the send method. ```ts await client.sendTextWithMentions( groupId, 'Hello @447700000000, can you check this?', false, ['447700000000@c.us'] ); ``` Set `hideTags` to `true` when you want WhatsApp to notify the users without showing the mention tags in the rendered text. ## Buttons, lists, and polls [#buttons-lists-and-polls] For new v5 integrations, use [`sendInteractive`](/docs/guides/interactive-messages) for buttons, lists, forms, carousels, or bookings. It requires an applied Insiders or higher license for the sending account. The guide covers the message format, response listeners, HTTP route, and recipient-side caveats. `sendButtons` and `sendListMessage` are deprecated legacy methods and remain Insiders-gated. `sendAdvancedButtons` is also an Insiders-gated legacy method. Check their exact contracts in the [sendButtons reference](/docs/reference/client/messages#sendbuttons) and [sendListMessage reference](/docs/reference/client/messages#sendlistmessage) before maintaining existing integrations. An available route does not prove that the session is licensed. For a fixed-choice flow without an Insiders gate, use the active [`sendPoll`](/docs/reference/client/messages#sendpoll) method when poll semantics fit the product. ```ts await client.sendPoll( chatId, 'Which day works best?', ['Monday', 'Tuesday', 'Wednesday'], { selectableCount: 1 } ); ``` Legacy button and list replies arrive as message events with types such as `buttons_response` or `list_response`. Interactive-message response events use the structured shapes described in the linked guide. Use `onPollVote` for decrypted vote updates and `getPollResults` for synced totals. See [Polls and message pins](/docs/guides/polls-and-pins) for closing times, hidden voter names, voting and message pinning. ## Forward messages [#forward-messages] ```ts await client.forwardMessages('1234567890@c.us', [messageId], true); ``` ## Acknowledgements [#acknowledgements] Subscribe before sending to observe delivery changes: ```ts const subscription = client.onAck((message) => { console.log(message.id, message.ack, message.body); if (message.sender) console.log('Sender:', message.sender.id); }); ``` The next release after 5.4.0 restores the full available message snapshot. It preserves fields supplied by WhatsApp, including the body, sender, chat, and extra metadata, without fetching a later version of the message. Some details may be absent while WhatsApp is loading it. In early v5 releases through 5.4.0, this callback instead receives `{ messageId, ack }`. | `message.ack` | Meaning | | ------------- | ----------------------------------- | | `-1` | Error | | `0` | Pending | | `1` | Accepted by the server | | `2` | Delivered to the recipient's device | | `3` | Read | | `4` | Played | Call `subscription.off()` to remove the handler. A returned send ID confirms submission; it does not mean that delivery or reading has occurred. ## Typing state [#typing-state] ```ts await client.simulateTyping(chatId, true); await client.simulateTyping(chatId, false); ``` ## Location messages [#location-messages] ```ts await client.sendLocation(chatId, latitude, longitude, 'London'); ``` When receiving a location message, read fields such as `lat`, `lng`, and `loc` from the incoming message payload. ## Errors [#errors] A message send can fail when the session is not ready or the chat ID is incorrect. It can also fail when the user is not available. WhatsApp can reject a large, malformed, or prohibited payload. ```ts import { SendTextError } from '@open-wa/wa-automate'; try { const messageId = await client.sendText(chatId, 'Hello'); console.log('Submitted:', messageId); } catch (error) { if (error instanceof SendTextError) { console.error(error.code, error.message); if (error.outcome === 'unknown') { console.error('Check the receiving chat before sending again.'); } } else { throw error; } } ``` `INVALID_ARGUMENT`, `LICENSE_REQUIRED`, and `SEND_REJECTED` report `outcome: 'not_sent'`. `SEND_FAILED` and `INVALID_SEND_RESULT` report `outcome: 'unknown'`; their `cause` retains the underlying failure or response. Common checks: | Error case | What to check | | --------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Invalid chat ID | Confirm the value matches a contact, group, or LID format. See **[ChatId primer](/docs/guides/chatid-primer)**. | | Session not ready | Wait for authentication and connection before sending. | | Media rejected | Check the MIME type, base64 data URL, filename, and file size. | | Message not delivered | Watch `onAck`; retry only from one bounded owner after reviewing the failure. | | Provider restriction | Pause or slow the relevant queue and retain the provider error. Timing alone does not prove that the account is clear. | ## Rate limits [#rate-limits] WhatsApp can limit or block accounts that send too many messages or send the same content again. It can also block accounts that contact users without consent. Send messages at a controlled rate. Do not send bulk messages. Do not use short retry loops. Read **[Rate limits](/docs/guides/rate-limits)** before running production automation. ## Full bot example [#full-bot-example] Create a project with the [initializer](/docs/getting-started/initializer), then replace `index.ts` with this example. Run `bun run start`, link the sending account if prompted, and send `!help` to it from your test chat. ```ts import { create } from '@open-wa/wa-automate'; const client = await create(); client.onMessage(async (message) => { if (message.body !== '!help') return; try { await client.sendText(message.from, 'I received your request.'); } catch (error) { console.error('Reply failed. Check the chat before retrying:', error); } }); let stopping = false; const shutdown = async () => { if (stopping) return; stopping = true; await client.stop(); }; process.once('SIGINT', () => void shutdown()); process.once('SIGTERM', () => void shutdown()); ``` The process stays connected to receive messages. Stop it with Ctrl+C when you finish; the saved connection remains available for the next run. # Multiple sessions (/docs/guides/multiple-sessions) # Multiple sessions [#multiple-sessions] Give each WhatsApp account a unique `sessionId` and persistent browser profile. For separate Easy API processes, also assign a unique port and API key. The [Easy API quick start](/docs/getting-started/easy-api) covers installation and API-key setup; use the [Docker guide](/docs/getting-started/docker) when each session needs its own persistent volume. ## One process per session [#one-process-per-session] Separate processes keep browser failures, restarts, and API credentials scoped to one account. The commands below target the published v5.1.0 CLI; specify `--host` and `--port` explicitly for that release. Before running them, set different, nonempty `SALES_API_KEY` and `SUPPORT_API_KEY` values in your secret store or shell environment. ```bash npx @open-wa/wa-automate@5.1.0 \ --session-id sales --host 127.0.0.1 --port 8081 --api-key "$SALES_API_KEY" npx @open-wa/wa-automate@5.1.0 \ --session-id support --host 127.0.0.1 --port 8082 --api-key "$SUPPORT_API_KEY" ``` Keep the API processes on loopback or a private network and configure your reverse proxy with a separate upstream credential for each session. `/health` is public and can include QR or diagnostic data, so do not expose it through a public proxy. See [Easy API security](/docs/getting-started/easy-api#security). ## One Node.js process [#one-nodejs-process] Use one process when your application needs direct control of each runtime. Create and retain one runtime per session, and stop each runtime during application shutdown. ```ts import { createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; const sessions = new Map(); for (const sessionId of ['sales', 'support']) { const runtime = await createClient({ sessionId, userDataDir: `./private/sessions/${sessionId}`, driver: new PuppeteerDriver(), headless: true, }); sessions.set(sessionId, runtime); await runtime.start(); } ``` Each runtime owns a browser context, so resource use grows with the number of active accounts. If one process exits, all its sessions stop; supervise and recover them as a unit or move sessions into separate processes when they need independent availability. ## Route requests to the intended session [#route-requests-to-the-intended-session] Keep the session-to-origin and session-to-key mapping in your application. Authenticate the upstream request with the key for that session and reject unknown session names before forwarding traffic. | Session | Local API origin | API key source | | --------- | ----------------------- | ----------------- | | `sales` | `http://127.0.0.1:8081` | `SALES_API_KEY` | | `support` | `http://127.0.0.1:8082` | `SUPPORT_API_KEY` | For example, a request to `sales` can be forwarded to the method route on its own API process: ```ts const salesApiKey = process.env.SALES_API_KEY; if (!salesApiKey) { throw new Error('Set SALES_API_KEY before sending requests'); } const response = await fetch('http://127.0.0.1:8081/api/sendText', { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': salesApiKey, }, body: JSON.stringify({ to: '447700900000@c.us', content: 'Your update is ready.', }), }); if (!response.ok) { throw new Error(`The sales session returned ${response.status}`); } ``` The exact input shape comes from the method contract; see the [`sendText` reference](/docs/reference/client/messages#sendtext). Do not let a user with access to one session inherit permission to send from another. ## Recovery and capacity [#recovery-and-capacity] Monitor each Easy API process separately and wait for `/health` to report `connected: true` and `session.ready: true` after startup or restart. A responding port establishes HTTP liveness only. Keep the session ID, profile directory, API key, logs, and restart policy paired so you can recover one account without changing another. Browser memory, CPU, media work, sockets, and reconnect bursts determine how many sessions a host can support. There is no fixed safe count; add sessions only after measuring the workload on the intended host. For process supervision, use your platform's service manager or container runtime. See [authentication and session recovery](/docs/guides/authentication-flow) for profile handling and [configuration and CLI](/docs/guides/configuration-and-cli) for runtime flags. # Node-RED integration (/docs/guides/node-red) # Node-RED integration [#node-red-integration] `@open-wa/node-red` connects Node-RED to an already running open-wa Easy API instance. The current package is `5.1.0` and registers exactly three node types: the `owa-server` configuration node, `Command`, and `Listen`. It does not provide separate Send, Receive, Session, or Media nodes. ## Prerequisites [#prerequisites] * Node-RED 5.x and Node.js available on the machine that runs the flow. * An authenticated open-wa session with Easy API enabled. * The Easy API base URL, for example `http://127.0.0.1:8080`. * The Easy API key if the session was started with `--api-key`. ## Install the supported package [#install-the-supported-package] Install into the Node-RED user directory so Node-RED can load it from its palette: ```bash cd ~/.node-red npm install @open-wa/node-red@5.1.0 node-red ``` Open `http://127.0.0.1:1880`, choose **Manage palette**, and confirm the `@open-wa/node-red` package is installed. The package name is `@open-wa/node-red`; the older `@open-wa/node-red-contrib-wa-automate` guide describes a different package and palette. ## Connect to Easy API [#connect-to-easy-api] Start an Easy API session separately. This loopback example requires Node-RED and Easy API to run on the same machine. Replace the API-key placeholder with a local secret before running it; for remote access, follow the [network and security guidance](/docs/operations/security-and-deployment) and use an API address reachable from the Node-RED host. ```bash export WA_API_KEY="replace-with-your-api-key" npx @open-wa/wa-automate@5.1.0 --session-id sales --host 127.0.0.1 --port 8080 --api-key "$WA_API_KEY" ``` In Node-RED, add the **owa-server** configuration node and fill these fields: | Field | Value | | ----- | ------------------------------------------------------------------------------- | | Name | A label such as `sales Easy API`. | | URL | The Easy API base URL, such as `http://127.0.0.1:8080`. | | Key | The same value passed to `--api-key`; leave empty only when the API has no key. | The node creates a `SocketClient` for that URL. In v5 the client uses HTTP RPC for commands and the event stream for listeners; the old node help text mentioning `--socket` is stale for this package. ## Current palette [#current-palette] ### `owa-server` configuration node [#owa-server-configuration-node] Stores `name`, `url`, and optional `key`. It reports connected, disconnected, and connection-error status and keeps the client available to the flow. ### `Command` [#command] The visible palette label is **Command** and the runtime type is `cmd`. It loads command names from `/meta/basic/commands`, then calls the selected method through the configured server. Set `method` and JSON `args` in the node, or provide `msg.method` and `msg.args` at runtime. A command waits for the server connection and defaults to a 30-second timeout; set `timeout` to `-1` only when an unbounded call is intentional. For `sendText`, the argument object is: ```json { "to": "1234567890@c.us", "content": "Hello from Node-RED" } ``` ### `Listen` [#listen] The visible palette label is **Listen** and the runtime type is `listen`. It loads listener names from `/meta/basic/listeners`, registers one listener against the server, and emits received data as `msg.payload`. Use `onMessage` for inbound message events; the listener is stopped when the node closes. ## Importable echo flow [#importable-echo-flow] Import this JSON from Node-RED's menu (**Import** → **Clipboard**). Edit the `url` and `key` on the `owa-server` configuration node before deploying. The flow listens for `onMessage` and sends `pong` only when an inbound text message is exactly `ping`, ignoring letter case and surrounding spaces. It sends a WhatsApp reply, so deploy it only when you intend to reply to matching messages. ```json [ { "id": "openwa-tab", "type": "tab", "label": "open-wa echo", "disabled": false, "info": "Echo inbound messages through the v5 Easy API client" }, { "id": "openwa-server", "type": "owa-server", "name": "sales Easy API", "url": "http://127.0.0.1:8080", "key": "REPLACE_WITH_API_KEY" }, { "id": "openwa-listen", "type": "listen", "z": "openwa-tab", "name": "incoming messages", "server": "openwa-server", "listener": "onMessage", "x": 170, "y": 120, "wires": [["openwa-format"]] }, { "id": "openwa-format", "type": "function", "z": "openwa-tab", "name": "make echo command", "func": "const message = msg.payload || {};\nif (!message.from || typeof message.body !== 'string' || message.body.trim().toLowerCase() !== 'ping') return null;\nmsg.method = 'sendText';\nmsg.args = { to: message.from, content: 'pong' };\nreturn msg;", "outputs": 1, "x": 390, "y": 120, "wires": [["openwa-command"]] }, { "id": "openwa-command", "type": "cmd", "z": "openwa-tab", "name": "sendText", "server": "openwa-server", "method": "sendText", "args": "{}", "timeout": "30", "x": 610, "y": 120, "wires": [["openwa-debug"]] }, { "id": "openwa-debug", "type": "debug", "z": "openwa-tab", "name": "send result", "active": true, "tosidebar": true, "complete": "payload", "x": 820, "y": 120, "wires": [] } ] ``` The `REPLACE_WITH_API_KEY` value is a placeholder, not a secret to commit. This node stores `key` as an ordinary flow property, not as a declared Node-RED credential. Do not commit or share an exported flow containing a real key, and restrict access to the Node-RED editor and flow exports. Confirm that the `owa-server` URL and key match the running Easy API. ## Troubleshooting [#troubleshooting] For the canonical readiness and authentication checks, see [Readiness and authentication checks](/docs/operations-and-troubleshooting/error-handling#readiness-and-authentication-checks). | Symptom | Command or observation | Expected result | Next action | | ----------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `owa-server` stays disconnected | `curl -sS http://127.0.0.1:8080/health` | HTTP `200` means the API is reachable; check `connected` and `session.ready` for the WhatsApp session state. `/health` is public and does not check the API key. | On connection refusal, start Easy API or correct URL and port. If the session is not ready, complete authentication before debugging Node-RED. Check a configured key with the metadata request below. | | Metadata request is `401` | `curl -i -H "X-API-Key: $WA_API_KEY" http://127.0.0.1:8080/meta/basic/commands` | The response lists command metadata. | Put the same key in **owa-server → Key**; an empty or different key is rejected. | | `Command` shows “Waiting for socket connection” | Check the `owa-server` status and the metadata request above. | The server node connects and the command list loads. | Correct the node URL or key, then redeploy. If Easy API is reachable but the session is not ready, finish WhatsApp authentication first. | | A matching `ping` gets no reply | Check the `Listen` node status and `/meta/basic/listeners` response. | The node should show `listening`, and `onMessage` should be listed. | Confirm the message body is `ping`, the session is ready, and the **Command** node uses `sendText`. Other messages are intentionally ignored by this flow. | | `sendText` times out | Inspect the `Command` status | It reports a timeout after the configured seconds. | Check the chat ID (`@c.us` or `@g.us`), session readiness, and Easy API logs; increase timeout only for a known slow operation. | ## Related [#related] * [Easy API quick start](/docs/getting-started/easy-api) * [Socket Client](/docs/client-and-integrations/socket-client) * [Webhooks for Business](/docs/guides/webhooks-for-business) # OpenUI chat messages (/docs/guides/openui) # OpenUI chat messages [#openui-chat-messages] OpenWA's OpenUI adapter gives agent plugins one message document for Neo and WhatsApp. Its catalogue contains `TextMessage`, `Receipt`, `BookingCard`, `ReplyButtons`, `ChoiceList` and `Form`. Neo renders the document in the existing Chat feed; the server compiles it to `sendText` or `sendInteractive`. This integration is available in the development source and is not part of the published 5.9.7 packages. ## Use it with Voxtral [#use-it-with-voxtral] No additional plugin or configuration flag is needed. Voxtral uses Jev to choose the format, validates the answer model's structured arguments, and creates an OpenUI document from them. The compiler validates that document before sending. The same document supplies the preview in Chat. Under **Plugins → Voxtral → Preview messages**, try the examples, edit a document, and submit simulated replies. This preview sends no WhatsApp messages. The normal Chat feed shows recipient controls as read-only, so the dashboard operator can't accidentally answer on the recipient's behalf. WhatsApp button, list and form replies keep their stable IDs. Voxtral correlates each response with its own sent message in the same chat and treats it as a conversation turn. During a call it answers through call audio; in voice-note mode it sends a voice note. See [Voxtral](/docs/guides/voxtral) for setup. ## Use the adapter in another plugin [#use-the-adapter-in-another-plugin] The helpers are exported from `@open-wa/plugin-sdk/openui` and work in JavaScript and TypeScript. Use your existing plugin runtime and the peer from its event: ```ts import { createOpenUIMessage, sendOpenUI } from '@open-wa/plugin-sdk/openui'; const source = createOpenUIMessage('ReplyButtons', { body: 'Would you like directions?', actions: [ { type: 'reply', id: 'directions', label: 'Yes please' }, { type: 'reply', id: 'no_directions', label: 'No thanks' }, ], }); const messageId = await sendOpenUI(runtime, peer, source); ``` `runtime` is supplied to the plugin's `init` function. A returned message ID confirms submission; use observed acknowledgements for delivery status. `sendOpenUI` makes one attempt and doesn't retry an uncertain send. For an agent that generates OpenUI Lang directly, supply `openUIPrompt` with the allowed components, then pass the same list to the compiler: ```ts import { compileOpenUI, openUIPrompt, sendOpenUI } from '@open-wa/plugin-sdk/openui'; const allowed = ['BookingCard'] as const; const instructions = openUIPrompt(allowed); // Give instructions to your model and collect its complete output in source. const message = compileOpenUI(source, allowed); await sendOpenUI(runtime, peer, message.source, allowed); ``` The host supplies the recipient separately. The document cannot select another chat or invoke a client method. The compiler rejects incomplete messages, unresolved references, unsupported components, queries, mutations, state declarations and invalid message fields. Rendering partial output never sends it. Normal runtime licensing and message validation still apply. ## Render in a Neo plugin [#render-in-a-neo-plugin] Neo supplies OpenUI and its own React instance to `createChat(host)` and `createPage(host)`, so browser modules need no separate React or OpenUI bundle: ```js export function createChat({ React, openui }) { return function AssistantChat({ children }) { const source = openui.createMessage('Receipt', { title: 'Appointment summary', fields: [{ label: 'Time', value: '2 pm' }], }); return children({ entries: [{ id: 'appointment-summary', at: Date.now(), content: React.createElement(openui.Message, { source }), }] }); }; } ``` Use a stable event ID and timestamp from your own conversation state in a real plugin. Supply `messageId` on a feed entry to attach it to the corresponding WhatsApp message. Respect the host's privacy setting before rendering content. `openui.Message` is read-only by default. An interactive application can supply `onAction(selection)`, which receives `{ type: 'reply', id }`, `{ type: 'list', id }`, or `{ type: 'form', answers }` after validating the choice against the document. The application owns authentication, correlation and any subsequent action. Local preview clicks aren't WhatsApp recipient responses. ## Booking dates and native rendering [#booking-dates-and-native-rendering] `BookingCard` uses the shared [interactive-message schema](/docs/guides/interactive-messages): `booking.startAt` and optional `endAt` are ISO timestamps with an offset. WhatsApp also requires a location, booking URL or phone number. The optional `timeZone` property controls Neo's display and is removed before native sending. Voxtral accepts civil dates and IANA timezones and computes these timestamps. OpenUI controls Neo's presentation. WhatsApp renders native messages using its own phone UI; a successful preview doesn't prove phone rendering. The shared Effect booking codec normalizes timestamps to UTC whole seconds during validation; the live Android check confirmed that this displays the date, time and location in the native details panel. The same codec handles SDK and HTTP sends, rejects invalid calendar dates and rejects ranges that collapse to the same second. The adapter uses OpenUI's [catalogue and renderer APIs](https://www.openui.com/docs/api-reference/react-lang) and its [framework-independent parser](https://github.com/thesysdev/openui/tree/main/packages/lang-core). The native booking mapping follows the documented [ISO date payload](https://wppconnect.io/wa-js/functions/chat.sendBookingMessage.html). # Voluntary phone-number sharing (/docs/guides/phone-number-sharing) # Voluntary phone-number sharing [#voluntary-phone-number-sharing] ```ts const requestId = await client.requestPhoneNumber('447700900123@c.us'); // Only when your application intends to disclose its own account number: const shareId = await client.sharePhoneNumber('447700900123@c.us'); ``` `requestPhoneNumber` sends WhatsApp's native share-number request to a direct LID chat. The recipient decides whether to share. It does not look up a hidden number or force the recipient to disclose it. `sharePhoneNumber` sends the native protocol message for sharing the signed-in account's own number and updates WhatsApp's per-contact sharing metadata. It accepts a destination only, never an arbitrary number to disclose. Both methods return a submitted message ID; ACKs indicate delivery, and recipient sharing is a separate action. Receiver behavior depends on WhatsApp's account and client support. Receiving a request does not confirm that the recipient's share response succeeded. Inspect the chat before retrying an uncertain submission. # Polls and message pins (/docs/guides/polls-and-pins) # Polls and message pins [#polls-and-message-pins] ## Create a poll [#create-a-poll] ```ts const pollId = await client.sendPoll(chatId, 'Which day?', [ 'Friday', 'Saturday', 'Sunday', ], { selectableCount: 1, endTime: Date.now() + 60 * 60 * 1000, hideVoterNames: true, quotedMsgId: messageId, }); ``` `selectableCount` defaults to 1; zero allows all choices. Choices must be nonempty and unique. WhatsApp enforces the account's native choice-count and text-length limits. `endTime` is **Unix milliseconds**, whereas WhatsApp event start/end times use Unix seconds. Closing times and hidden voter names require WhatsApp to enable those native sender features for the account/chat. The patch uses WhatsApp's payload builder, which selects `pollCreationMessageV6` for those options. Unsupported sender features throw an explicit error; these options are not silently discarded. Recipient rendering of V6 depends on its WhatsApp version and configuration. `hideVoterNames` describes WhatsApp's display behavior. It does not promise cryptographic anonymity. The SDK omits individual votes and identities from results for these polls. ## Read results and vote [#read-results-and-vote] ```ts if (typeof pollId === 'string') { const results = await client.getPollResults(pollId); console.log(results.options); // [{ id, name, count }, ...] await client.voteOnPoll(pollId, [results.options[0].id]); await client.voteOnPoll(pollId, []); // Remove this account's vote. } ``` Use the returned option IDs, rather than deriving them from display text. Voting rejects duplicate/unknown choices, too many selections and closed polls. Successful native completion returns `true`, which is not a delivery receipt. Results hydrate WhatsApp's local vote store and return `scope: 'synced'`; unsynced votes may be absent. Counts include locally pending votes and exclude known failed sends. These APIs support direct chats and groups. Channel polls use a different native vote store and are not exposed by these methods. ## Listen for votes [#listen-for-votes] ```ts const listener = client.onPollVote(async (vote) => { console.log(vote.pollId, vote.selectedOptionIds); const results = await client.getPollResults(vote.pollId); console.log(results.voterCount, results.options); }); // When this subscription is no longer needed: listener.off(); ``` The listener reads decrypted native votes, including this account's changes. History hydration can emit votes too; it is not exclusively a new incoming message notification. Deduplicate ordinary votes by `id` and `timestamp`, and use results for current totals. Hidden-voter notifications omit `id` and `senderId`. A removed vote has an empty `selectedOptionIds` array. ## Pin a message [#pin-a-message] ```ts await client.pinMessage(messageId); // Seven days by default. await client.pinMessage(messageId, 'OneDay'); await client.pinMessage(messageId, 'ThirtyDays'); await client.unpinMessage(messageId); ``` Message pins affect the conversation for everyone and are subject to WhatsApp's chat permissions. `pinChat(chatId)` and `unpinChat(chatId)` instead control the current account's chat list. These are different operations. # Rate Limits and Ban Risk (/docs/guides/rate-limits) # Rate Limits and Ban Risk [#rate-limits-and-ban-risk] open-wa does not expose a WhatsApp rate-limit guarantee. WhatsApp does not publish a stable per-chat or per-group rate table, and provider restrictions can change without notice, so a local queue is a pacing control rather than a ban avoidance mechanism. This project is unofficial and has no affiliation with WhatsApp or Meta. Any automation can be restricted or banned by the provider. Consent, a bounded queue, and explicit handling for rejected sends reduce avoidable load but do not guarantee delivery or account safety. ## What is known and what is not [#what-is-known-and-what-is-not] The client and schema define how open-wa validates and dispatches a send. They do not define WhatsApp's server-side quota, rolling window, account-age rule, or restriction duration. Treat community measurements as dated observations that need their own evidence; this guide does not turn them into defaults. Provider rejection can happen even when a local queue is obeying its schedule. Handle the rejected operation, stop or slow the relevant workflow, and record the provider error before deciding whether a retry is appropriate. Do not retry in a short loop, and do not assume that a random delay makes a pattern safe. ## Queue concurrency and rate are different [#queue-concurrency-and-rate-are-different] `concurrency` limits how many queue tasks run at the same time. It does not set the time between tasks. In `p-queue`, `intervalCap` and `interval` implement an interval window; omitting `intervalCap` leaves the cap at its default of Infinity. The following example uses strict rolling-window mode to allow no more than one queued send in any 15-second span, runs one task at a time, and records rejected jobs: ```ts import PQueue from 'p-queue'; import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; const runtime = await createClient({ sessionId: 'paced-sender', driver: new PuppeteerDriver(), }); const client = new Client({ client: runtime, transport: runtime.getTransport(), }); const sendQueue = new PQueue({ concurrency: 1, intervalCap: 1, interval: 15_000, strict: true, }); async function enqueueSend(chatId: string, text: string) { try { const result = await sendQueue.add(() => client.sendText(chatId, text)); if (result === false) { console.error('WhatsApp did not accept the send request', { chatId }); return false; } return result; } catch (error) { console.error('WhatsApp rejected a queued send', { chatId, error }); throw error; } } await client.start(); await enqueueSend('447700900000@c.us', 'Hello'); ``` Install the example dependencies with `npm install @open-wa/wa-automate@5.1.0 @open-wa/driver-puppeteer@5.1.0 p-queue`. Complete the normal client authentication when it starts. This is a process-local schedule: it does not coordinate other sessions or senders and does not establish a provider quota. Choose the interval for your workflow, consent model, and operational budget, then stop or reclassify work when WhatsApp reports a restriction. ## Safer workflow boundaries [#safer-workflow-boundaries] * Accept messages or jobs only from a bounded source so a traffic spike cannot create an unbounded backlog. * Keep consent and opt-out state beside the job, and remove opted-out recipients before enqueueing a send. * Keep one clear owner for retries. A queue retry and an application retry can multiply traffic if both act on the same rejection. * Record the chat ID, operation, result, and provider error without logging message contents or credentials by default. * Stop or pause the queue after repeated rejections and require an operator to decide whether the workflow should resume. ## Handling a restriction [#handling-a-restriction] There is no documented universal wait time for a WhatsApp restriction. Stop the automation, retain the provider warning or error, and review consent, queue depth, duplicate sends, retry behavior, and account context. Resume only after you have a bounded plan and a reason to believe the provider state allows it; timing alone cannot establish that the account is clear. ## Related [#related] * **[AI Agent Patterns](/docs/guides/ai-agent-patterns)**: Separate model concurrency from outbound pacing * **[Messages guide](/docs/guides/messages)**: Handle send results and provider failures * **[p-queue interval options](https://github.com/sindresorhus/p-queue#options)**: `intervalCap`, `interval`, and `concurrency` # S3 media storage (/docs/guides/s3-media) # S3 media storage [#s3-media-storage] `@open-wa/integration-s3@5.1.0` listens for `message.received`, decrypts media that has a `deprecatedMms3Url`, uploads it through `pico-s3`, and adds `cloudUrl` to the message object. It does not remove or replace the original WhatsApp fields. This package exports the `s3Plugin(config)` factory; it is registered directly with `createClient()` rather than loaded from the CLI's string-based `plugins` list. ## Configure the plugin [#configure-the-plugin] Install the public client, Puppeteer driver, and integration in the Node.js project: ```bash npm install @open-wa/wa-automate@5.1.0 @open-wa/driver-puppeteer@5.1.0 @open-wa/integration-s3@5.1.0 ``` Use a provider value accepted by the integration source. `provider`, `accessKeyId`, `secretAccessKey`, and `bucket` are required; `region` is optional. ```js // app.mjs import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; import { createClient } from '@open-wa/wa-automate'; import { s3Plugin } from '@open-wa/integration-s3'; const client = await createClient({ sessionId: 'sales', driver: new PuppeteerDriver(), plugins: [s3Plugin({ provider: 'aws', bucket: process.env.OPENWA_MEDIA_BUCKET, region: process.env.OPENWA_MEDIA_REGION || 'eu-west-2', accessKeyId: process.env.OPENWA_MEDIA_ACCESS_KEY_ID, secretAccessKey: process.env.OPENWA_MEDIA_SECRET_ACCESS_KEY, public: false, directory: 'DATE_CHAT', ignoreHostAccount: true, headers: { 'Cache-Control': 'private, max-age=3600' }, })], }); await client.start(); ``` The `provider` union currently contains `aws`, `gcp`, `do`, `wasabi`, and `backblaze`. The integration source has no `endpoint` option, so do not copy an endpoint-based configuration from a generic S3 SDK. Provider credentials, region rules, bucket policy, and object URL behavior still belong to the provider you select. ## What changes on a message [#what-changes-on-a-message] The plugin runs only when all of these conditions are true: 1. The message has `deprecatedMms3Url` and `mimetype`. 2. A `decryptMedia` client is available to the plugin. 3. `ignoreHostAccount` does not exclude a message sent by the host account. 4. Decryption and upload succeed. On success, the plugin keeps the original WhatsApp fields and assigns `cloudUrl` to the same message object after its asynchronous decrypt and upload finish. It does not emit a separate upload-complete event. Since event listeners run independently of that asynchronous hook, a `message.received` listener must not assume `cloudUrl` is already set when it runs. ## Await a URL in your own message handler [#await-a-url-in-your-own-message-handler] Use one upload path for a message. If later work needs the URL immediately, use the integration's exported `S3Uploader` in your own awaited handler instead of registering `s3Plugin` for the same messages: ```js import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; import { S3Uploader } from '@open-wa/integration-s3'; const mediaConfig = { provider: 'aws', bucket: process.env.OPENWA_MEDIA_BUCKET, region: process.env.OPENWA_MEDIA_REGION || 'eu-west-2', accessKeyId: process.env.OPENWA_MEDIA_ACCESS_KEY_ID, secretAccessKey: process.env.OPENWA_MEDIA_SECRET_ACCESS_KEY, public: false, directory: 'DATE_CHAT', ignoreHostAccount: true, }; const runtime = await createClient({ sessionId: 'sales', driver: new PuppeteerDriver(), }); const client = new Client({ client: runtime, transport: runtime.getTransport(), }); const uploader = new S3Uploader(mediaConfig, { error: (message) => console.error('S3 upload failed', message), }); function getBase64Payload(dataUrl) { const match = typeof dataUrl === 'string' ? /^data:[^,]+;base64,([A-Za-z0-9+/]+={0,2})$/.exec(dataUrl) : null; if (!match) throw new Error('decryptMedia did not return a base64 data URL'); return match[1]; } client.onMessage(async (message) => { if (!message.deprecatedMms3Url || !message.mimetype) return; const cloudUrl = await uploader.uploadMedia(message, { decryptMedia: async (mediaMessage) => { return getBase64Payload(await client.decryptMedia(mediaMessage)); }, }); if (cloudUrl) { console.info('media copy ready', { messageId: message.id, cloudUrl }); } }); await client.start(); ``` This path awaits `uploadMedia()` and receives either the generated URL or `null`; the uploader logs decryption and upload failures. During application shutdown, wait for `uploader.waitForQueue()` and then call `uploader.close()`. `cloudUrl` is the URL generated by `pico-s3` for the configured provider, bucket, directory, and object name. The original `deprecatedMms3Url` may expire or require WhatsApp access, so consumers that need later access should store `cloudUrl` together with the message ID and the access policy they use. ## Directory and throughput options [#directory-and-throughput-options] `directory` accepts one of these built-in strategies: | Value | Example key prefix | | ----------- | ------------------------ | | `DATE` | `2026-09-29/` | | `CHAT` | `1234567890/` | | `DATE_CHAT` | `2026-09-29/1234567890/` | | `CHAT_DATE` | `1234567890/2026-09-29/` | An arbitrary string is used as a literal directory value. The upload queue has capacity for 64 files, runs at most two uploads concurrently, and is rate-limited to two submissions per second. A busy session can therefore queue work rather than complete the upload inline. If decryption or upload fails, the plugin logs `Upload error` and leaves `cloudUrl` unset for that message. It does not provide a durable retry journal or a separate failure event. If your workflow must track unsuccessful copies, add an application-owned record and recovery path; the integration alone does not guarantee archival. ## Access control for `cloudUrl` [#access-control-for-cloudurl] The plugin passes `public` to `pico-s3` when uploading and generating the URL, but that option alone does not establish effective access control. Keep the bucket and object policy private where required, and verify how your selected provider serves the generated URL before exposing it to a CRM or browser. Provider-specific signed URL expiry and scope must be configured with that provider. `public: true` asks the upload library for a public object URL. Use it only when the bucket policy, object metadata, and retention policy explicitly allow public access, because anyone who obtains that URL may be able to read the media. ## Provider support boundary [#provider-support-boundary] The integration advertises the five provider identifiers above through its type contract. Exact endpoint, ACL, signed URL, egress, and lifecycle behavior is provider-specific and is not established by this integration alone. Confirm a provider with a representative upload, private read, expiry, and deletion exercise before making it part of a production workflow. ## Compliance boundary [#compliance-boundary] Uploading media to object storage gives you a copy and a place to apply storage controls. It does not by itself establish healthcare, financial, GDPR, legal-hold, or other regulatory compliance. Your deployment must define retention, deletion, access review, encryption, audit evidence, regional residency, and incident response for the data and provider you choose. ## Related [#related] * [Plugin getting started](/docs/plugins/getting-started) * [Logging and audit trails](/docs/guides/logging-and-audit) * [Security and deployment](/docs/operations/security-and-deployment) # Send a WhatsApp message with the API (/docs/guides/send-message-api) # Send a WhatsApp message with the API [#send-a-whatsapp-message-with-the-api] This guide shows two ways to send a WhatsApp message with open-wa. Use the Node.js library when your application runs in Node.js or Bun. Use the Easy API when your application uses another language or runs on a different machine. The two paths use a WhatsApp account that you link to your own server. open-wa is not the official WhatsApp Business Platform. It connects through WhatsApp Web. ## Before you start [#before-you-start] * Install Node.js 22 or later. * Keep a phone with WhatsApp near you to link the account. * Get the phone number of a test recipient that already has a chat with your account. To start a chat with a new number, you must apply a Restricted or Insiders license. A recipient with an existing chat does not need a license. See [licensed features](/docs/licensing/licensed-features). ## Find the recipient chat ID [#find-the-recipient-chat-id] open-wa sends to a chat ID, not to a plain phone number. For a person, write the full international number without `+`, spaces, or dashes. Then add `@c.us` to the end. | Recipient | Chat ID | | ----------------------------- | -------------------------------------- | | UK number +44 7123 456789 | `447123456789@c.us` | | Group | The `@g.us` ID from the group metadata | | Sender of an incoming message | The exact `message.from` value | If an incoming message gives a `message.from` value, use that value without changes. It can end in `@lid`. Read the [chat ID primer](/docs/guides/chatid-primer) for all ID types. ## Send from Node.js [#send-from-nodejs] Install the library in your project: ```bash npm install @open-wa/wa-automate ``` Create `send.mts` with this code: ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'sender' }); const messageId = await client.sendText('447123456789@c.us', 'Hello from open-wa'); console.log('Submitted message', messageId); await client.stop(); ``` Run the file. On the first run, scan the QR code with WhatsApp on your phone. open-wa keeps the login for the next run. ```bash npx tsx send.mts ``` `sendText` returns a serialized message ID when WhatsApp accepts the message. The ID confirms submission. It does not confirm delivery. To monitor delivery, subscribe to `onAck` before you send. See [acknowledgements](/docs/guides/messages#acknowledgements). ## Send through the HTTP API [#send-through-the-http-api] Start the Easy API on the machine that holds the WhatsApp session: ```bash npx @open-wa/wa-automate --session-id sender --host 127.0.0.1 --port 8080 --api-key "your-api-key" ``` Scan the QR code that the terminal shows. Then send a `POST` request to `/api/messages/sendText`: ```bash curl -X POST http://localhost:8080/api/messages/sendText \ -H "Content-Type: application/json" \ -H "X-API-Key: your-api-key" \ -d '{"to":"447123456789@c.us","content":"Hello from open-wa","options":{}}' ``` A successful response has `success: true`. The `data` value is the send result. ```json { "success": true, "data": "" } ``` The same request works from any language that can send HTTP requests. This example uses Python with `requests`: ```python import requests response = requests.post( "http://localhost:8080/api/messages/sendText", headers={"X-API-Key": "your-api-key"}, json={"to": "447123456789@c.us", "content": "Hello from open-wa", "options": {}}, timeout=30, ) response.raise_for_status() print(response.json()) ``` The Easy API also serves interactive API docs at `http://localhost:8080/api-docs/`. Use these docs to find the request body for each method. ## Reply to a received message [#reply-to-a-received-message] To reply in the same chat, use the sender chat ID from the incoming message. To quote the message, give its ID to `reply`. ```ts client.onMessage(async (message) => { if (message.body === 'ping') { await client.reply(message.from, 'pong', message.id); } }); ``` For a complete bot, read [build a WhatsApp bot](/docs/guides/whatsapp-bot). ## Troubleshooting [#troubleshooting] | Problem | Cause | Fix | | --------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- | | The request returns `401` | The `X-API-Key` header does not match `--api-key`. | Send the same key that you gave to the CLI. | | The send fails for a new number | The account has no chat with that number. | Apply a Restricted or Insiders license, or let the recipient send a message first. | | The error has `outcome: 'unknown'` | WhatsApp did not confirm the result. | Check the chat before you send again. open-wa does not retry `sendText`. | | The number does not receive the message | The chat ID format is incorrect. | Remove `+`, spaces, and dashes. Add `@c.us` to the end. | WhatsApp can restrict an account that sends many messages to new contacts. To decrease this risk, send messages only to contacts that expect them, and obey the [rate limits](/docs/guides/rate-limits). ## Related [#related] * [Messages and message events](/docs/guides/messages) * [WhatsApp webhooks](/docs/client-and-integrations/webhook-payloads) * [Quick start](/docs/getting-started/quickstart) * [Client API reference](/docs/reference/client/client#sendtext) # Session events (/docs/guides/session-events) # Session events [#session-events] Use `ev` when several files need the same event helper, or when you need to observe authentication before `create()` returns. Every import shares the same emitter in the current process. Use `client.events` when you want the native events from one client. The Effect hybrid and optional PubSub APIs described below are available from OpenWA 5.6.0. ## Observe startup events [#observe-startup-events] Register the listener before starting the session: ```ts import { create, ev } from '@open-wa/wa-automate'; ev.on('client.ready', (_event, sessionId) => { console.log(`Session ${sessionId} is ready`); }); const client = await create({ sessionId: 'support' }); ``` `client.ready` follows authentication and runtime finalization. For Easy API, also wait for `/health` to report both `connected: true` and `session.ready: true` before sending. See [readiness](/docs/getting-started/quickstart#3-check-readiness). Native events keep their core payload as the first argument. The second argument identifies the session, and the third contains the event name: ```ts ev.on('session.connection.disconnected', (event, sessionId) => { console.warn(`Session ${sessionId} disconnected`, event.details?.reason); }); ``` ## Share application events between files [#share-application-events-between-files] In `jobs.ts`, register a listener: ```ts import { ev } from '@open-wa/wa-automate'; ev.on('app.job.finished', ({ jobId }) => { console.log('Finished:', jobId); }); ``` In `index.ts`, load that module before emitting the event: ```ts import './jobs'; import { ev } from '@open-wa/wa-automate'; ev.emit('app.job.finished', { jobId: 'daily-summary' }); ``` Run `bun run index.ts`. It prints `Finished: daily-summary`. `emit()` invokes listeners immediately, in the same process. It does not store or replay events for modules that subscribe later. Use `await ev.emitAsync(...)` when your application needs to wait for its asynchronous listeners. ## Use v4 wildcard subscriptions [#use-v4-wildcard-subscriptions] `*` matches one segment of an event name; `**` matches zero or more. Both trailing and leading patterns work, including `qr.**` and `**.support`. Supported message and state listener aliases use `..` and carry the familiar envelope: ```ts ev.on('onAck.**', ({ sessionId, data: message }) => { console.log(sessionId, message.id, message.ack); }); ev.on('onMessage.support.**', ({ data: message }) => { console.log('Incoming message:', message.id); }); ``` The envelope contains `{ ts, sessionId, id, event, data }`. Its `id` identifies the event; `data.id` identifies the message. Aliases are provided for `onMessage`, `onAnyMessage`, `onAck`, `onPollVote`, `onEvent`, `onEventResponse`, `onStateChanged`, `onLogout`, `onInteractiveResponse`, and `onFormResponse`. An alias does not add a missing WhatsApp capability; consult the [migration guide](/docs/getting-started/migrating-to-v5#known-gaps) for unsupported listeners. `eventMode` defaults to `true`. Set it to `false` in `create()` or `createClient()` to disable the automatic listener aliases. Native lifecycle events and your own application events remain available. A catch-all `**` subscription sees both native events and their aliases, so choose one naming scheme when processing a message once. ## Authentication and session data [#authentication-and-session-data] Lifecycle aliases use `.` and call listeners with `(data, sessionId, namespace)`: | Pattern | First argument | | ---------------------- | -------------------------------------------------------------------------- | | `qr.**` | The pairing string for your QR renderer. | | `linkCode.**` | The generated link code. | | `sessionData.**` | The encrypted compact session object after durable storage accepts a save. | | `sessionDataBase64.**` | The same compact session JSON encoded as base64. | The session object contains current v5's encrypted `auth` and `data` values. It does not recreate v4 token fields. Custom encryption keys are not included in these events. Keep pairing and session data inside a private application; never forward them to shared logs, analytics, or public endpoints. For native event names, QR generation is `launch.auth.qr.generated`, with the value in `event.details.qr`. Link-code generation is `launch.auth.linkCode.generated`, with the value in `event.details.linkCode`. See [link-code login](/docs/getting-started/link-code) and the [event reference](/docs/reference/events) for those flows. ## Remove a subscription [#remove-a-subscription] Keep the callback reference so you can remove just your listener: ```ts const onFinished = ({ jobId }: { jobId: string }) => { console.log('Finished:', jobId); }; ev.on('app.job.finished', onFinished); ev.off('app.job.finished', onFinished); ``` `once()` removes its subscription after the first matching event. Stopping a client detaches that session's forwarding connection. Other sessions and listeners registered by other files keep working. ### Use an Effect scope [#use-an-effect-scope] If your application uses Effect, `onGlobalEvent()` owns a subscription for its current scope and removes it automatically when the scope closes: ```ts import { Effect } from 'effect'; import { ev, onGlobalEvent } from '@open-wa/wa-automate'; await Effect.runPromise(Effect.scoped(Effect.gen(function* () { yield* onGlobalEvent('app.job.finished', ({ jobId }) => { console.log('Finished:', jobId); }); ev.emit('app.job.finished', { jobId: 'daily-summary' }); }))); // The scoped listener has now been removed. ``` The helper uses the same synchronous emitter. It does not create a queue or fiber per message. Session forwarding is also owned by the runtime's Effect scope and is removed after shutdown or failed startup. See [Effect scopes](https://effect.website/docs/resource-management/scope/) for the resource-lifetime model. ### Await Effect listeners [#await-effect-listeners] Listeners can return a Promise or an Effect. `emit()` still calls ordinary listeners synchronously; Effect and Promise work belongs to the emitter's scope. `emitAsync()` waits for listener work and returns the results in listener order. Use `yield* ev.emitEffect(name, ...args)` inside Effect to wait without starting another root runtime. `ev.onScoped()` and `ev.onAnyScoped()` also interrupt in-flight listener work when their scope closes. ### Observe through PubSub [#observe-through-pubsub] `ev.pubsub()` exposes a native Effect PubSub for queued observers. `ev.subscribe()` provides a scoped subscription, and `ev.stream()` provides a Stream for filtering, batching, or sequential processing. Ordinary callbacks retain their synchronous behavior. ```ts import { Effect, PubSub } from 'effect'; import { ev } from '@open-wa/wa-automate'; await Effect.runPromise(Effect.scoped(Effect.gen(function* () { const subscription = yield* ev.subscribe(); ev.emit('app.job.finished', { jobId: 'daily-summary' }); const { event, args } = yield* PubSub.take(subscription); console.log(event, args[0]); }))); ``` Queued packets contain `{ event, args }`, preserving every callback argument, including session metadata. The default hub is created on first use, has no replay, and retains pending events for active subscribers without a memory cap. Choose an explicit finite policy on first access, for example `ev.pubsub({ strategy: 'sliding', capacity: 256 })`, if retaining the newest observations is preferable to keeping a backlog. Dropping and sliding can lose observations; `pubsubStatus` reports rejected emitter publications. Emitter methods reach callbacks and queued observers. Publishing directly with `PubSub.publish(ev.pubsub(), packet)` reaches only queued observers. `emitAsync()` waits for callback results, so queued consumers control their own completion and retry policies. A subscription ends with its Effect scope; stopping one client detaches its forwarding without closing other sessions' global subscriptions. For complete standalone examples and consumer cleanup, read [Consume events with Effect](/docs/guides/effect-event-streams). Exact methods, buffer options, and counters are in the [HyperEmitter reference](/docs/reference/hyperemitter). # Voxtral voice assistant (/docs/guides/voxtral) # Voxtral voice assistant [#voxtral-voice-assistant] Use the official `@open-wa/integration-voxtral` plugin to answer WhatsApp voice notes and audio calls with a conversational assistant. Add it to your Easy API configuration, supply a Mistral API key, and describe how the assistant should respond. The plugin handles transcription, conversation context, answer generation, and speech. Voice-note replies arrive as WhatsApp voice notes. Calls use a live audio connection, so callers can speak naturally and interrupt the assistant. ## 1. Create your voice assistant [#1-create-your-voice-assistant] Use **Node.js 22.21.1 or newer** and npm: ```bash npm init @open-wa@latest voice-assistant -- --mode easy-api --plugins voxtral cd voice-assistant ``` The wizard asks for JavaScript or TypeScript, your browser, and whether Voxtral should handle **voice notes**, **calls**, or **both**. Or use the [project builder](/new?mode=easy-api\&plugins=voxtral) to choose visually, preview the files, and copy a complete command. The generated `wa.config.js` or `wa.config.ts` includes the plugin and your chosen mode. Easy API discovers it automatically; you don't need a config flag or a separate plugin installation command. ## 2. Add your keys [#2-add-your-keys] Copy `.env.example` to `.env` and set your [Mistral API key](https://console.mistral.ai/api-keys/): ```dotenv MISTRAL_API_KEY=YOUR_MISTRAL_API_KEY # Enable intent-based chat actions with Jev JEV_API_KEY=YOUR_JEV_API_KEY ``` Get a Jev key from [TypeSafe](https://typesafe.ai/) to let the assistant choose chat actions. Without it, voice conversations continue and chat actions are unavailable. The plugin reads both keys from the environment. For calls, also fill in `OPENWA_LICENSE_KEY` with your calling-enabled open-wa license. The generated config loads `.env`. Mistral bills transcription, answer generation, and speech separately through your account; see [Mistral's current pricing](https://mistral.ai/pricing). ## 3. Start the assistant [#3-start-the-assistant] ```bash npm start ``` Only selected plugins are installed, on demand at startup, then reused from the project's cache. The browser follows your selected reuse/download policy. Link WhatsApp using the QR code shown by Easy API, then open the [dashboard](http://localhost:8080/dashboard/) and select **Plugins → Voxtral**. The plugin runs inside Easy API and uses its connected WhatsApp session. It keeps working when you close the dashboard. Send a voice note to the linked account from another WhatsApp account. The assistant answers with a voice note. Send a follow-up such as “Can you explain the second point?” and it uses the earlier conversation to answer. ## Add Voxtral to an existing project [#add-voxtral-to-an-existing-project] Set `MISTRAL_API_KEY` in the environment that starts Easy API. Add the plugin to your existing `wa.config.js` or `wa.config.ts`: ```js plugins: ['@open-wa/integration-voxtral'], ``` It starts with the default voice-note assistant. Add `pluginConfig.voxtral` to customize it, or edit its settings in Neo. Instructions accept a string or an array; the plugin joins array entries for you. ## Choose voice notes, calls, or both [#choose-voice-notes-calls-or-both] Set `mode` in `pluginConfig.voxtral`, or change **Conversation mode** on the Voxtral dashboard page: | Mode | Behavior | | ------------- | ----------------------------------------------------------------------------------------------------------- | | `voice-notes` | Answer incoming voice notes with voice notes. This is the default. | | `calls` | Answer incoming WhatsApp audio calls with a live voice assistant. Voice notes remain in the ordinary inbox. | | `both` | Handle voice notes and audio calls with the same assistant settings. | The assistant handles direct conversations. It ignores groups, broadcasts, status updates, and its own messages. In `voice-notes` or `both` mode, you can also enable `voiceNotes.acceptText` to answer ordinary direct text messages. ### Voice-note conversations [#voice-note-conversations] Each completed recording becomes a conversation turn. The plugin decrypts it, recognizes the speech, generates an answer, and sends prepared Ogg Opus audio using WhatsApp's voice-note presentation. You don't need to install FFmpeg or write a media handler. The assistant remembers recent turns for the same person in the same WhatsApp session. Context expires after 30 minutes without activity by default and is cleared when Easy API restarts. Choose **Clear AI context** in the person's Chat view to clear a person's current context immediately. To include a readable copy of the assistant's reply and let people type their follow-ups, add these settings inside `voxtral`: ```js voiceNotes: { includeText: true, acceptText: true, }, ``` `includeText` sends the assistant's answer as text alongside its voice note. It doesn't send the incoming transcript back as the answer. ### Live audio calls [#live-audio-calls] Switch to `calls` or `both` and supply your calling-enabled open-wa license in the top-level Easy API config: ```bash export OPENWA_LICENSE_KEY='YOUR_CALLING_LICENSE' ``` ```js title="wa.config.js" export default { licenseKey: process.env.OPENWA_LICENSE_KEY, plugins: ['@open-wa/integration-voxtral'], pluginConfig: { voxtral: { mode: 'both', }, }, }; ``` Run `npm start` as before. Calling uses open-wa's [licensed audio calling support](/docs/guides/calls). If calling isn't available, the dashboard shows the reason; the Easy API and any enabled voice-note handling continue running. With `answer: 'auto'`, the assistant answers incoming calls and plays the greeting after the call connects. With `answer: 'manual'`, incoming calls ring in the dashboard and you choose **Answer with Voxtral**, **Answer myself**, or **Reject**. To place a call, choose a contact in **Calls**, select **Voxtral assistant** as the audio source, and choose **Call**. In **Chat**, open the person and choose **Call with Voxtral**. The caller can interrupt a spoken answer. The assistant stops its current speech, listens, and answers the new turn. **Take over** switches the live call to your microphone and speakers; **Return to assistant** gives the conversation back. These controls are also available in the dashboard's floating call control. open-wa supports one active call per WhatsApp session. During human takeover, the assistant can keep transcribing the caller. Your microphone is not transcribed. Add an optional handoff summary before returning the call to Voxtral if it needs facts from your side of the conversation. In `both` mode, a person's voice notes and calls share recent conversation context. Voice notes received from that person during their call wait until the call ends, so the assistant doesn't answer them in two places at once. ## Choose chat actions with Jev [#choose-chat-actions-with-jev] During a call or voice-note exchange, the person can ask: “Send me the booking confirmation in this WhatsApp chat.” Jev classifies that request in the recent conversation context and chooses the appropriate format. Voxtral fills the selected format with known details, sends it to the same person, then uses the actual send result for its spoken acknowledgement. | Intent | Available action | | --------------------------------------------- | ----------------------------------------- | | A written answer, summary or link | Text message | | An order confirmation or receipt | Formatted confirmation template | | A few quick choices or link/call/copy actions | Button message | | Several known choices, such as services | Choice list | | Collect several answers | Form | | Appointment or reservation details | Native booking card | | “No, I said 2 pm” after a booking card | Corrected card replacing the previous one | | Ordinary conversation | No chat action | | An unclear chat request | Ask a clarifying question | You can correct the latest booking card during the call or with another voice note. Voxtral changes only the corrected fields and keeps the other details. It sends the replacement before removing the old card, and reports if the old card couldn't be removed. This corrects the message in the chat; it doesn't change a reservation in an external booking system. The original card must still be in the assistant's recent conversation context. Set `JEV_API_KEY` on the Easy API host. The classifier model defaults to `jev-latest`; change it under **Edit assistant → Chat actions**. Rich formats require an Insiders or higher license with interactive messaging. Jev is given only the formats the session supports. A confirmation template here is formatted WhatsApp text, not a Meta-approved business template. Jev reviews each user turn, proposed message content, action result and spoken reply. It uses the conversation and observed outcomes to choose the next step: continue speaking, perform an unfinished action, repair rejected arguments, ask for a missing fact or reconcile an uncertain send. A request can produce several messages, such as a booking card followed by buttons, without repeating the completed action. The same loop handles voice notes, calls and interactive replies. Message contents are built from the shared OpenUI schemas. Chat actions are enabled by default. Turn **Send messages when asked** off under **Plugins → Voxtral → Edit assistant → Chat actions**, or set `actions: { sendMessage: false }` inside `pluginConfig.voxtral`. **Choose format** previews Jev's decision using your saved model without sending a WhatsApp message. The normal Chat feed uses the shared [OpenUI catalogue](/docs/guides/openui) for message previews. **Plugins → Voxtral → Preview messages** lets you try cards, buttons, lists, forms and their replies without sending a WhatsApp message. The feed shows the selected format, a details preview and the message's submission and observed delivery state. Button, list and form replies to the assistant's own messages become conversation turns. During a call, the assistant responds through call audio; in voice-note mode it sends a voice note. In calls-only mode, selections made outside a call remain in context for the next call. These reply controls don't require `voiceNotes.acceptText`. The assistant asks only for missing required details. A booking card needs a date, local time, timezone and a location, booking URL or phone number; it does not require a name or email. The plugin calculates timestamps and the displayed date/time using the timezone rules for the appointment date, including daylight saving. If the clocks make a local time ambiguous or nonexistent, it asks for clarification. Forms and buttons do not require booking details. Sending a card, template or button doesn't create, verify or cancel a reservation in a booking system. An uncertain send isn't retried automatically; pausing the assistant or taking over a call stops new actions. If Jev is unavailable, the assistant continues speaking but explains that it can't perform chat actions. The selected answer model must support [Mistral function calling](https://docs.mistral.ai/studio/conversations/function-calling). ## Manage the assistant in the dashboard [#manage-the-assistant-in-the-dashboard] Open a person in **Chat** to see transcripts under their voice notes, assistant replies and delivery states, and call turns in the ordinary message timeline. The conversation toolbar shows the current assistant state and controls. Open **Plugins → Voxtral** for session settings, mode, voice preview, and usage; its conversation list links into the same Chat view. | Control | What it does | | ---------------------------- | ------------------------------------------------------------------------------------------------------ | | **Conversation mode** | Choose Voice notes, Calls, or Both. | | **Pause AI** | Stop new automatic responses across the session. Active calls remain available for takeover or ending. | | **Edit assistant** | Change instructions, voice, language, and reply settings. | | **Send messages when asked** | Let Jev choose the chat format and the assistant send agreed details during calls and voice notes. | | **Choose format** | Preview the Jev action decision without sending a WhatsApp message. | | **Preview voice** | Play a sample locally without sending a WhatsApp message. This uses your Mistral account. | | **Open chat** | Open the person's normal Chat feed from the plugin's conversation list. | | **Pause AI here** | Leave this chat to a human while the assistant handles others. | | **Clear AI context** | Clear this person's current assistant context without deleting WhatsApp messages. | | **Usage** | See processed audio duration, generated speech characters, and reported model token usage. | The voice-note activity view shows **Queued**, **Transcribing**, **Thinking**, **Generating voice**, **Sending**, and the observed message acknowledgement. Calls show their connection state separately from **Listening**, **Thinking**, and **Speaking**, so you can tell whether the call connected and whether the assistant is producing audio. Usage counters show the reporting period and provider values where available. Live transcription duration and interrupted requests may be missing; these counters are not a Mistral invoice. Dashboard settings are saved for this session and override matching values in your config file after a restart. Resetting settings removes those saved overrides. Use **Copy config** to move the effective settings into your file; the exported example references environment variables instead of including API keys. Voice and instruction changes apply to the next conversation turn. Mode changes affect new work; they don't silently hang up an active call. ## Configuration reference [#configuration-reference] To customize the assistant in your config file, add `pluginConfig`. For example: ```js pluginConfig: { voxtral: { agent: { instructions: [ 'You are a patient language tutor.', 'Keep replies short and ask one question at a time.', ], }, }, }, ``` These fields belong in `pluginConfig.voxtral`: | Field | Default | Purpose | | --------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------- | | `apiKey` | `MISTRAL_API_KEY` environment variable | Optional explicit Mistral API key; normally the plugin reads the environment itself. | | `jevApiKey` | `JEV_API_KEY` environment variable | Optional explicit Jev key for chat action classification. | | `mode` | `'voice-notes'` | `'voice-notes'`, `'calls'`, or `'both'`. | | `agent.instructions` | A brief, helpful assistant | A string or array of strings. The plugin joins array entries in order with line breaks. | | `agent.model` | `'mistral-small-latest'` | The Mistral language model that generates answers. | | `voice.id` | `'voice_478'` | Speech voice; use **Preview voice** to hear it. | | `language` | `'auto'` | Detect the person's language, or use a supported language code. | | `voiceNotes.includeText` | `false` | Send the assistant's answer as text as well as audio. | | `voiceNotes.acceptText` | `false` | Treat incoming direct text messages as assistant turns too. | | `calls.answer` | `'auto'` | Answer incoming calls automatically, or wait for a dashboard action with `'manual'`. | | `calls.greeting` | An introduction identifying the AI assistant | The first spoken message after a call connects. | | `actions.sendMessage` | `true` | Allow Jev-selected chat actions for the person in the current conversation. | | `actions.classifierModel` | `'jev-latest'` | Jev model for turn planning, action selection and response review. | | `conversation.idleTimeoutSeconds` | `1800` | How long recent conversation context remains after the last turn. | | `models.transcription` | `'voxtral-mini-transcribe-3-0'` | Transcription model for completed voice notes. | | `models.realtimeTranscription` | `'voxtral-mini-transcribe-realtime-3-0'` | Transcription model for live calls. | | `models.speech` | `'voxtral-mini-tts-3-0'` | Speech model used to produce replies. | | `contextBias` | `[]` | Names and specialist terms to help the transcription model recognize. | Partial settings inherit defaults for omitted fields, including fields inside `agent`, `voiceNotes`, `calls`, and `actions`. You don't need to copy a complete settings object to change one option. With `language: 'auto'`, the plugin configures language detection for each provider request. For better recognition of difficult recordings, set `models.transcription` to `'voxtral-small-transcribe-3-0'`. Choose a voice suitable for the response language from [Mistral's voice library](https://docs.mistral.ai/studio/audio/text-to-speech/voices). Model access and supported languages follow your Mistral account and the selected models. See the [speech-to-text](https://docs.mistral.ai/studio/audio/speech-to-text/overview) and [text-to-speech](https://docs.mistral.ai/studio/audio/text-to-speech/overview) references. ## Conversation data and recovery [#conversation-data-and-recovery] Recent transcripts and answers are held in memory for the conversation's idle timeout. The plugin doesn't save raw voice notes or call recordings by default. Settings, pause choices, usage counters, and message-processing identifiers are saved locally for the WhatsApp session. Audio and conversation text needed for responses are sent to Mistral. When chat actions are enabled, Jev receives the latest request, a bounded excerpt of recent conversation, proposed message content, action outcomes and proposed replies to choose the next action and review its response. Jev classification uses your TypeSafe account separately from Mistral. If a voice note contains no recognizable speech, the assistant asks the person to send it again or type their message. If Mistral rejects a key or reports a limit, the dashboard shows the affected service and a recovery action. A failed reply isn't labelled as delivered, and the plugin doesn't automatically repeat a WhatsApp send whose result is uncertain. If answer generation reports that the selected model has zero request quota, open **Plugins → Voxtral → Edit assistant** and change **Answer model** to a model your Mistral account can use. In file config, set `agent.model` inside `pluginConfig.voxtral`; for example, use `ministral-3b-latest` when your account has quota for it. Waiting won't resolve a model whose allowed quota is zero. If a live call loses its AI audio connection, the call control offers **Take over**, **Reconnect assistant**, and **End call**. Reconnecting starts a fresh AI audio connection on the existing call; it doesn't redial the person. ## Related guides [#related-guides] * [Easy API quick start](/docs/getting-started/easy-api) * [Calls](/docs/guides/calls) * [Media handling](/docs/guides/media) * [Plugin configuration](/docs/plugins/getting-started#configure-the-plugin) ## Text conversations with Jev [#text-conversations-with-jev] Voxtral uses the shared Jev decision and action engine. Add `@open-wa/integration-jev` to `plugins` to also answer ordinary text messages, or select Jev alongside Voxtral in the setup wizard. They share recent context, booking cards and send outcomes. During a live call, button and form replies return through call audio. See the [Jev guide](/docs/guides/jev) for setup, correction rules, dates and decision evidence. # Webhooks for Business (/docs/guides/webhooks-for-business) # Webhooks for Business [#webhooks-for-business] Use the v5 webhook integration when a CRM, helpdesk, queue, or backend should receive WhatsApp events. The sender makes HTTP `POST` requests; your receiver owns authentication, persistence, deduplication, and any slower business work. ## Prerequisites [#prerequisites] * An Easy API project running `@open-wa/wa-automate@5.1.0`. * A public HTTPS receiver URL that can accept `POST /webhooks/open-wa`. * One non-empty random value shared only by the sender and receiver as `OPEN_WA_WEBHOOK_SECRET`. The current package is `@open-wa/integration-webhook@5.1.0`. The CLI `--webhook` flag is parsed for compatibility but does not register the v5 delivery plugin. ## Configure the sender [#configure-the-sender] ```bash npm install @open-wa/integration-webhook@5.1.0 export OPEN_WA_WEBHOOK_SECRET='use-a-long-random-value' ``` Create `wa.config.mjs` in the directory from which you will run the CLI: ```js // wa.config.mjs const webhookSecret = process.env.OPEN_WA_WEBHOOK_SECRET?.trim(); if (!webhookSecret) throw new Error('OPEN_WA_WEBHOOK_SECRET is required'); export default { sessionId: 'sales', host: '127.0.0.1', port: 8080, plugins: ['@open-wa/integration-webhook'], pluginConfig: { webhook: { url: 'https://receiver.example/webhooks/open-wa', events: ['message.received', 'session.state.changed'], headers: { 'X-Webhook-Secret': webhookSecret }, retries: 3, retryDelay: 1000, timeout: 30000, queueCapacity: 1000, overload: 'backpressure', // Set enabled: true only when restart replay is required and this process // can write the journal path. durability: { enabled: false, path: '.openwa/webhook-deliveries.sqlite', replayLimit: 1000, }, }, }, }; ``` Start the session with the config file: ```bash npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id sales --host 127.0.0.1 --port 8080 ``` ## Run the receiver [#run-the-receiver] The receiver below is the canonical recipe also shown in [Webhook payloads](/docs/client-and-integrations/webhook-payloads). It validates its secret at startup, checks the header before parsing, bounds the body, returns useful failure codes, and logs only routing identifiers by default. ```js // receiver.mjs import express from 'express'; import Database from 'better-sqlite3'; import { createHash, timingSafeEqual } from 'node:crypto'; const expectedSecret = process.env.OPEN_WA_WEBHOOK_SECRET?.trim(); if (!expectedSecret) { throw new Error('OPEN_WA_WEBHOOK_SECRET must be set before the receiver starts'); } const expectedBytes = Buffer.from(expectedSecret, 'utf8'); const inbox = new Database('./openwa-webhook-inbox.sqlite'); inbox.exec(` CREATE TABLE IF NOT EXISTS webhook_inbox ( idempotency_key TEXT PRIMARY KEY, session_id TEXT NOT NULL, event TEXT NOT NULL, received_at INTEGER NOT NULL, envelope TEXT NOT NULL ) `); const insertInbox = inbox.prepare(` INSERT OR IGNORE INTO webhook_inbox (idempotency_key, session_id, event, received_at, envelope) VALUES (?, ?, ?, ?, ?) `); const app = express(); const rawBodies = new WeakMap(); const allowRawDiagnostics = process.env.NODE_ENV !== 'production' && process.env.DEBUG_WEBHOOK_RAW === '1'; function authenticate(req, res, next) { const supplied = req.get('X-Webhook-Secret'); if (!supplied || supplied.length !== expectedSecret.length) { return res.status(401).send('Unauthorized'); } const suppliedBytes = Buffer.from(supplied, 'utf8'); if (suppliedBytes.length !== expectedBytes.length || !timingSafeEqual(suppliedBytes, expectedBytes)) { return res.status(401).send('Unauthorized'); } next(); } function isRecord(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function parseEnvelope(value) { if (!isRecord(value)) return null; if (typeof value.webhookId !== 'string' || !value.webhookId) return null; if (typeof value.sessionId !== 'string' || !value.sessionId) return null; if (typeof value.event !== 'string' || !value.event) return null; if (typeof value.timestamp !== 'number' || !Number.isFinite(value.timestamp)) return null; if (!('payload' in value)) return null; return value; } app.post('/webhooks/open-wa', authenticate, express.json({ limit: '2mb', // Temporary local diagnosis only. This is disabled when NODE_ENV is production. verify: (req, _res, buffer) => { if (allowRawDiagnostics) rawBodies.set(req, buffer.toString('utf8')); }, }), (req, res) => { const envelope = parseEnvelope(req.body); if (!envelope) return res.status(400).send('Invalid open-wa webhook envelope'); if (allowRawDiagnostics) { console.warn('[LOCAL ONLY] raw webhook body:', rawBodies.get(req) ?? '(empty body)'); } const messageId = envelope.payload?.message?.id; const idempotencyKey = req.get('Idempotency-Key') || (typeof messageId === 'string' && messageId ? `${envelope.sessionId}:${envelope.event}:${messageId}` : createHash('sha256').update(JSON.stringify(envelope)).digest('hex')); // Insert the authenticated event before acknowledging it. A duplicate key is a no-op. const result = insertInbox.run( idempotencyKey, envelope.sessionId, envelope.event, Date.now(), JSON.stringify(envelope), ); console.info('webhook inbox updated', { sessionId: envelope.sessionId, event: envelope.event, duplicate: result.changes === 0, }); res.sendStatus(204); }); app.use((error, _req, res, _next) => { if (error?.type === 'entity.too.large') return res.status(413).send('Webhook body too large'); if (error?.type === 'entity.parse.failed') return res.status(400).send('Malformed JSON'); console.error('webhook receiver error', { name: error?.name ?? 'Error' }); res.status(500).send('Webhook receiver error'); }); app.listen(3000, () => console.log('Webhook receiver listening on http://localhost:3000')); ``` Install the receiver dependencies and run it separately from the Easy API: ```bash npm install express better-sqlite3 export OPEN_WA_WEBHOOK_SECRET='use-a-long-random-value' node receiver.mjs ``` Before connecting WhatsApp, confirm the receiver rejects unsafe requests and accepts one valid envelope: ```bash # Missing secret -> HTTP 401 curl -i -X POST http://localhost:3000/webhooks/open-wa \ -H 'Content-Type: application/json' -d '{}' # Malformed JSON with the right secret -> HTTP 400 curl -i -X POST http://localhost:3000/webhooks/open-wa \ -H 'Content-Type: application/json' \ -H "X-Webhook-Secret: $OPEN_WA_WEBHOOK_SECRET" -d '{' # Valid envelope -> HTTP 204 curl -i -X POST http://localhost:3000/webhooks/open-wa \ -H 'Content-Type: application/json' \ -H "X-Webhook-Secret: $OPEN_WA_WEBHOOK_SECRET" \ -d '{"webhookId":"local-test","sessionId":"sales","event":"message.received","payload":{"message":{"id":"local-1","from":"1234567890@c.us","body":"Hello"}},"timestamp":1700000000123}' ``` ## Map an event [#map-an-event] The v5 envelope uses `payload`, not the older `data` field. Start with the message sender, text or caption, and message ID: | Business value | v5 path | | ----------------- | ------------------------------------ | | Sender | `payload.message.from` | | Text | `payload.message.body` | | Media caption | `payload.message.caption` | | Deduplication key | `sessionId:event:payload.message.id` | For Zapier or Make, map those paths from the parsed JSON body. In n8n, a Code node can normalize them: ```js return [{ json: { sessionId: $json.sessionId, event: $json.event, sender: $json.payload?.message?.from, text: $json.payload?.message?.body || $json.payload?.message?.caption, messageId: $json.payload?.message?.id, }, }]; ``` ## Delivery limits [#delivery-limits] The sender treats network errors, timeouts, and non-`2xx` responses as failures. The configuration above makes one request plus three retries, with delays of `1000ms`, `2000ms`, and `4000ms`; requests time out after `30000ms`, and the in-memory waiting queue holds up to `1000` deliveries with `10` concurrent sends. After retry exhaustion, the default in-memory configuration logs the failure and keeps no record for automatic replay. With `durability.enabled: true`, pending rows in `.openwa/webhook-deliveries.sqlite` replay after a later startup up to `replayLimit`, and exhausted rows are marked dead letter. Durable requests carry an `Idempotency-Key`; when durability is disabled, this receiver derives a message key from the session, event, and message ID, or hashes the exact envelope for other event types. Duplicates remain possible, and neither retry mode promises eventual delivery. ## Related [#related] * [Webhook payloads reference](/docs/client-and-integrations/webhook-payloads) * [Easy API quick start](/docs/getting-started/easy-api) * [Security and deployment](/docs/operations/security-and-deployment) # Build a WhatsApp bot in Python (/docs/guides/whatsapp-bot-python) # Build a WhatsApp bot in Python [#build-a-whatsapp-bot-in-python] This guide shows how to build a WhatsApp bot in Python. open-wa runs the WhatsApp session in Node.js. Your Python application receives messages through a webhook and replies through the HTTP API. ```text WhatsApp -> open-wa Easy API -> webhook -> Python app -> POST /api/messages/sendText ``` ## Before you start [#before-you-start] * Install Node.js 22 or later for the open-wa process. * Install Python 3.10 or later. * Keep a phone with WhatsApp near you to link the account. ## 1. Create the Python receiver [#1-create-the-python-receiver] Install Flask and requests: ```bash pip install flask requests ``` Create `bot.py`. The receiver checks the shared secret, reads the message, and replies through the Easy API. ```python import hmac import os import requests from flask import Flask, abort, request API_URL = "http://127.0.0.1:8080" API_KEY = os.environ["OPEN_WA_API_KEY"] WEBHOOK_SECRET = os.environ["OPEN_WA_WEBHOOK_SECRET"] app = Flask(__name__) def send_text(chat_id: str, text: str) -> None: response = requests.post( f"{API_URL}/api/messages/sendText", headers={"X-API-Key": API_KEY}, json={"to": chat_id, "content": text, "options": {}}, timeout=30, ) response.raise_for_status() @app.post("/webhooks/open-wa") def webhook(): secret = request.headers.get("X-Webhook-Secret", "") if not hmac.compare_digest(secret, WEBHOOK_SECRET): abort(401) envelope = request.get_json(silent=True) if not envelope: abort(400) if envelope.get("event") == "message.received": message = envelope["payload"]["message"] body = (message.get("body") or "").strip().lower() if body == "!help": send_text(message["from"], "Commands: !help, !hours") elif body == "!hours": send_text(message["from"], "We are open Monday to Friday, 09:00 to 17:00.") return "", 204 if __name__ == "__main__": app.run(host="127.0.0.1", port=5000) ``` Start the receiver before you start open-wa: ```bash export OPEN_WA_API_KEY='use-a-long-random-value' export OPEN_WA_WEBHOOK_SECRET='use-a-different-long-random-value' python bot.py ``` ## 2. Configure open-wa [#2-configure-open-wa] In a new directory, install the webhook integration: ```bash npm install @open-wa/integration-webhook ``` Create `wa.config.mjs`: ```js const webhookSecret = process.env.OPEN_WA_WEBHOOK_SECRET?.trim(); if (!webhookSecret) throw new Error('OPEN_WA_WEBHOOK_SECRET is required'); export default { sessionId: 'python-bot', host: '127.0.0.1', port: 8080, plugins: ['@open-wa/integration-webhook'], pluginConfig: { webhook: { url: 'http://127.0.0.1:5000/webhooks/open-wa', events: ['message.received'], headers: { 'X-Webhook-Secret': webhookSecret }, }, }, }; ``` ## 3. Start open-wa [#3-start-open-wa] Set the same two values in this terminal. Then start the Easy API: ```bash export OPEN_WA_API_KEY='use-a-long-random-value' export OPEN_WA_WEBHOOK_SECRET='use-a-different-long-random-value' npx @open-wa/wa-automate --config ./wa.config.mjs --session-id python-bot --host 127.0.0.1 --port 8080 --api-key "$OPEN_WA_API_KEY" ``` Scan the QR code with WhatsApp on your phone. Then send `!help` to the linked account from another phone. The bot replies in the same chat. ## Respond quickly [#respond-quickly] open-wa waits for the webhook response. A slow response uses a delivery slot and can cause a retry after a timeout. Return `204` quickly. For slow work, put the message in a queue and send the reply from a worker. A retry can send the same event again. To prevent a duplicate reply, store `payload.message.id` and ignore an ID that you already processed. ## Send messages without a webhook [#send-messages-without-a-webhook] To send a notification from a Python script, call the HTTP API directly. You do not need the webhook for this task. ```python import requests requests.post( "http://127.0.0.1:8080/api/messages/sendText", headers={"X-API-Key": "use-a-long-random-value"}, json={"to": "447123456789@c.us", "content": "Your order has shipped.", "options": {}}, timeout=30, ).raise_for_status() ``` Read [send a WhatsApp message with the API](/docs/guides/send-message-api) for the chat ID format and license rules. The API key gives full control of the WhatsApp account. Do not expose port `8080` to the internet. Keep the Easy API on `127.0.0.1` or behind a private network. ## Related [#related] * [WhatsApp webhooks](/docs/client-and-integrations/webhook-payloads) * [Build a WhatsApp bot in Node.js](/docs/guides/whatsapp-bot) * [Security and deployment](/docs/operations/security-and-deployment) * [Rate limits](/docs/guides/rate-limits) # Build a WhatsApp bot (/docs/guides/whatsapp-bot) # Build a WhatsApp bot [#build-a-whatsapp-bot] This guide shows how to build a WhatsApp bot that runs on your own server. The bot reads incoming messages, replies to commands, and sends events to other systems. All examples use the open-wa Node.js library. open-wa links a normal WhatsApp account through WhatsApp Web. You do not need a Meta business account, a template approval, or a cloud provider. You are responsible for the server and for the account. ## How a bot works [#how-a-bot-works] A WhatsApp bot has three parts: 1. **A connected session.** open-wa keeps a WhatsApp Web session open for your account. 2. **Event handlers.** Your code subscribes to events such as `onMessage` and decides what to do. 3. **Actions.** Your code calls client methods such as `sendText` and `reply`, or calls other systems. The bot process must stay running to receive messages. Run it as a service, in Docker, or with a process manager. ## Create the project [#create-the-project] Run the initializer to create a project with the library and a start script: ```bash bunx --bun @open-wa/wa-automate init my-bot ``` For an existing Node.js project, install the library: ```bash npm install @open-wa/wa-automate ``` Read the [initializer guide](/docs/getting-started/initializer) for the generated files. ## Reply to commands [#reply-to-commands] Replace `index.ts` with this example. It replies to three commands and ignores all other messages. ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'bot' }); const commands: Record = { '!help': 'Commands: !help, !hours, !price', '!hours': 'We are open Monday to Friday, 09:00 to 17:00.', '!price': 'See our current prices at https://example.com/prices', }; client.onMessage(async (message) => { const reply = commands[message.body?.trim().toLowerCase() ?? '']; if (!reply) return; try { await client.reply(message.from, reply, message.id); } catch (error) { console.error('Reply failed. Check the chat before you send again:', error); } }); ``` Start the bot. On the first run, scan the QR code with WhatsApp on your phone. Then send `!help` to the linked account from another phone. ## Ignore groups and your own messages [#ignore-groups-and-your-own-messages] `onMessage` does not include messages that your account sends. To ignore group chats, check `isGroupMsg`. ```ts client.onMessage(async (message) => { if (message.isGroupMsg) return; // Handle direct messages here. }); ``` To work with group messages, read the [groups guide](/docs/guides/groups) and the [group filtering guide](/docs/guides/group-filtering). ## Automate replies with your own data [#automate-replies-with-your-own-data] A bot handler can call any system that your server can access. This example looks up an order status and sends the result. ```ts client.onMessage(async (message) => { const match = message.body?.match(/^order\s+(\d+)$/i); if (!match) return; const response = await fetch(`https://shop.example/api/orders/${match[1]}`); if (!response.ok) { await client.reply(message.from, 'I cannot find that order.', message.id); return; } const order = (await response.json()) as { status: string }; await client.reply(message.from, `Order ${match[1]}: ${order.status}`, message.id); }); ``` Keep each handler short. For slow work, put the message in a queue and reply when the work completes. ## Send events to another backend [#send-events-to-another-backend] Your bot can also run without custom code in the session process. The webhook integration sends each WhatsApp event to an HTTP endpoint. Your backend then calls the Easy API to reply. This pattern lets you write the bot logic in any language. ```text WhatsApp -> open-wa Easy API -> webhook -> your backend -> POST /api/messages/sendText ``` Read these guides to set up this pattern: * [WhatsApp webhooks](/docs/client-and-integrations/webhook-payloads) shows the setup and the event payload. * [Webhooks for business](/docs/guides/webhooks-for-business) shows a receiver with authentication. * [Build a WhatsApp bot in Python](/docs/guides/whatsapp-bot-python) shows a complete Python receiver. ## Add buttons, lists, and polls [#add-buttons-lists-and-polls] Interactive messages make a bot faster to use. Read [interactive messages](/docs/guides/interactive-messages) and [polls and pins](/docs/guides/polls-and-pins) for the supported message types. ## Connect an AI agent [#connect-an-ai-agent] To let an AI model read and send WhatsApp messages, use the open-wa MCP server. Read the [MCP guide](/docs/guides/mcp) and [AI agent patterns](/docs/guides/ai-agent-patterns). To add a conversational voice assistant through an Easy API plugin, follow [Voxtral voice assistant](/docs/guides/voxtral) for two-way voice notes and calls. ## Stop the bot safely [#stop-the-bot-safely] Stop the client when the process receives a stop signal. This action closes the browser and keeps the saved login. ```ts let stopping = false; const shutdown = async () => { if (stopping) return; stopping = true; await client.stop(); }; process.once('SIGINT', () => void shutdown()); process.once('SIGTERM', () => void shutdown()); ``` WhatsApp can restrict an account that sends unwanted messages. To decrease this risk, reply only to users that contact the bot, and obey the [rate limits](/docs/guides/rate-limits). ## Related [#related] * [Send a WhatsApp message with the API](/docs/guides/send-message-api) * [Messages and message events](/docs/guides/messages) * [WhatsApp events](/docs/guides/whatsapp-events) * [Best practices](/docs/operations-and-troubleshooting/best-practices) # WhatsApp events and RSVPs (/docs/guides/whatsapp-events) # WhatsApp events and RSVPs [#whatsapp-events-and-rsvps] Use a ready client to send native WhatsApp event cards and handle attendance responses. Events use the common live patch and don't require a licence. The SDK exposes seven methods and two listeners. ## Create and update an event [#create-and-update-an-event] ```ts const chatId = '447700900123@c.us'; const startTime = Math.floor(Date.now() / 1000) + 3600; const eventId = await client.sendEvent(chatId, { name: 'Community meetup', startTime, endTime: startTime + 3600, description: 'Join us for a chat', location: 'London', extraGuestsAllowed: true, }); await client.editEvent(eventId, { description: 'Updated meetup details', endTime: null, }); const event = await client.getEvent(eventId); console.log(event?.name, event?.isCanceled); // If the meetup is canceled: await client.cancelEvent(eventId); ``` Only the creator can edit or cancel an event. Omitted update fields keep their existing values; `endTime: null` removes the end time, and an empty description or location clears that field. A canceled event remains in history. Existing call links retain their call type when edited. ## Schedule a call [#schedule-a-call] ```ts const callEventId = await client.sendEvent(chatId, { name: 'Planning call', startTime, endTime: startTime + 1800, callType: 'video', // or 'voice' isScheduledCall: true, }); console.log((await client.getEvent(callEventId))?.joinLink); ``` WhatsApp's current Web scheduled-call flow creates an event with a native call link and the scheduled-call flag. `callType` generates that link; `isScheduledCall` requires a call type. `requireApproval` can be passed to native link creation. If link creation fails, the API rejects before sending the event. A call link does not start a call. Receiving an event card or RSVP does not confirm that the receiving client exposes a Join control. `sendEvent` returns the submitted message ID. `editEvent`, `cancelEvent` and `respondToEvent` return `true` after WhatsApp's native send action completes. These results aren't recipient delivery receipts: use `onAck` for delivery and read status. The SDK doesn't retry mutations automatically. If a browser connection drops during a send, inspect the chat and current event before retrying. ## Read invitations and attendance [#read-invitations-and-attendance] ```ts const firstPage = await client.getEvents(chatId, { limit: 20 }); const nextPage = firstPage.nextCursor ? await client.getEvents(chatId, { limit: 20, before: firstPage.nextCursor }) : null; const invitation = firstPage.events.find(event => !event.fromMe && !event.isCanceled); if (invitation) { // Make this choice in your application or ask the user first. await client.respondToEvent(invitation.id, 'going'); const attendance = await client.getEventResponses(invitation.id); console.log(attendance); } ``` Outgoing RSVP choices are `going`, `notGoing` and `maybe`. Reads can also return `unknown` when WhatsApp supplies an unfamiliar value. `getEventResponses` returns the latest explicit RSVP per participant, including decrypted responses hydrated from local history. The creator's implicit attendance isn't added as an invented response. History is the linked Web device's locally synced history, newest first. Page limits are 1–100, and cursors must belong to the same chat. `getEvent` returns `null` for an event absent from available history; event mutations reject when their target can't be found. ## Listen for event changes and RSVPs [#listen-for-event-changes-and-rsvps] ```ts const eventListener = client.onEvent(event => { console.log(event.id, event.name, event.isCanceled); }); const attendanceListener = client.onEventResponse(response => { console.log(response.eventId, response.senderId, response.response); }); // When the application no longer needs these subscriptions: eventListener.off(); attendanceListener.off(); ``` `onEvent` reports native event additions and changes, including this account's sends. `onEventResponse` observes WhatsApp's decrypted RSVP collection. Loading history may emit callbacks, so keep response IDs and timestamps if processing attendance changes must be idempotent. Runtime reinjection disposes browser listeners before binding the new generation. Structured message snapshots also expose the event under `message.event`. The per-client event stream uses `event.changed` with `{ event, ctx }` and `event.response` with `{ response, ctx }`; the shared emitter uses the aliases `onEvent` and `onEventResponse` with its usual session namespace. ## Times, schemas and persistence [#times-schemas-and-persistence] Event start/end times and event creation timestamps are **Unix seconds**. RSVP timestamps are **milliseconds**. Event creation needs a nonblank name and a future start time; an end time must be at or after the start time. Effect schemas are exported from `@open-wa/schema` and `@open-wa/client`: `EventInputSchema`, `EventUpdateSchema`, `WhatsAppEventSchema`, `EventResponseSchema`, `EventResponseTypeSchema`, `EventListOptionsSchema` and `EventPageSchema`. The method and listener registries derive their serving schemas from these contracts. ```ts import { serializeWhatsAppEvent, deserializeWhatsAppEvent, serializeEventResponse, deserializeEventResponse, } from '@open-wa/client'; const snapshot = await client.getEvent(eventId); if (!snapshot) throw new Error('Event not found in synced history'); const savedEvent = serializeWhatsAppEvent(snapshot); const restoredEvent = deserializeWhatsAppEvent(savedEvent); const responses = await client.getEventResponses(eventId); const savedResponses = responses.map(serializeEventResponse); const restoredResponses = savedResponses.map(deserializeEventResponse); ``` These JSON serializers validate the event contracts and exclude message secrets, encryption keys and encrypted RSVP bytes. ## API methods [#api-methods] | Method | Result | Purpose | | ----------------------------------- | ----------------------- | ---------------------------------------------- | | `sendEvent(to, event)` | `MessageId` | Create a native event card. | | `getEvent(eventId)` | `WhatsAppEvent \| null` | Read one event. | | `getEvents(chatId, options?)` | `EventPage` | Page through synced event history. | | `getEventResponses(eventId)` | `EventResponse[]` | Read the latest explicit RSVP per participant. | | `editEvent(eventId, changes)` | `boolean` | Update an event created by this account. | | `cancelEvent(eventId)` | `boolean` | Cancel an event created by this account. | | `respondToEvent(eventId, response)` | `boolean` | Submit or change this account's RSVP. | The [generated method reference](/docs/reference/client/whatsapp-events) includes Easy API routes under `/api/events`, SocketClient examples, parameters and return models. The [listener reference](/docs/reference/client/events) lists `onEvent` and `onEventResponse`. # WhatsApp API documentation for Node.js | open-wa (/docs) # open-wa / wa-automate [#open-wa--wa-automate] open-wa is a self-hosted WhatsApp API and Node.js library for developers. Use these docs to link a WhatsApp account, send messages from your application, receive events, and connect your helpdesk. ## Start here [#start-here] * Use **[Quick start](/docs/getting-started/quickstart)** if you want to start the API, authenticate WhatsApp, and send your first message. * Use **[Custom Code](/docs/getting-started/custom-code)** if you are embedding the library in your own Node.js project. * Use **[Socket Client](/docs/client-and-integrations/socket-client)** if you want to connect to a remote Easy API instance. * Use **[Cloudflare Session Proxy](/docs/client-and-integrations/cf-proxy)** if you want remote session access without exposing a public port. ## What you can build [#what-you-can-build] * Customer support inboxes that sync WhatsApp into your own tools * Order, booking, and delivery notifications from internal systems * Bots that react to messages, group activity, and runtime events * Webhook bridges for CRMs, helpdesks, automations, and low-code tools * Multi-session automations with named accounts and isolated consumers * AI-agent workflows through the built-in Model Context Protocol server ## Prerequisites [#prerequisites] * **Node.js** >= 22.21.1 * **npm** for released packages, or **pnpm 11.9.0** for repo development * A WhatsApp account on a phone that can stay connected ## Version covered by these docs [#version-covered-by-these-docs] These docs cover **v5**, which has stable releases. See the [changelog](/changelog) for the current release and its changes. Use the version installed in your application when comparing method contracts. Choose the `alpha` tag only for deliberate prerelease work. ## Documentation map [#documentation-map] * **Getting Started**, first-run guides for Quick start, Easy API, Docker, custom code, and link-code login. * **Guides**, practical guides for config, events, sessions, messages, media, and groups. * **Client & Integrations**, SocketClient, Chatwoot, proxying, and Cloudflare session proxy. * **Concepts**, the runtime model and core terminology. * **API Reference**, generated API docs from the active reference set. * **Operations & Troubleshooting**, error handling, logout recovery, and production best practices. * **Licensing**, licensed features and feature-availability notes. ## Legal and Risk Context [#legal-and-risk-context] This project is unofficial and has no affiliation with WhatsApp or Meta. You accept the risk when you use it. When you use this project, you agree to the Terms of Service. * Automation tools can cause an account restriction or ban * Check your local laws regarding cryptographic software before use * See the [Hippocratic + Do Not Harm License](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) for full terms ## Support [#support] * **Documentation**: [https://openwa.dev](https://openwa.dev) * **Discord**: [https://discord.gg/dnpp72a](https://discord.gg/dnpp72a) * **Get a license key**: [Licence options](/docs/licensing/pricing) ## If you came from the old docs [#if-you-came-from-the-old-docs] Start from the task you need to finish. Use Easy API for a running local API, Custom Code for `createClient`, Socket Client for remote consumers, and the generated reference for exact methods. # Licensed features (/docs/licensing/licensed-features) # Licensed features [#licensed-features] open-wa has license-gated functions. This page explains how the runtime applies a key and how to check the exact behavior you plan to use. Review the current first-party offer, then validate the specific restricted or insiders behavior in the host-account session you actually plan to run. ## What licensing affects [#what-licensing-affects] License-gated behavior includes features such as: * certain advanced or insiders-only capabilities * restricted flows like messaging non-contact numbers * premium/support-tier unlocks tied to a host account The method tier and the behavior available in a deployed release are separate checks. A key does not make an absent method available, and the host account must still support the operation. ## Current behavior [#current-behavior] At launch, the runtime resolves the configured `licenseKey`. When the host account context is available, it sends the key to the license validation service. The runtime classifies the result before it marks the client ready. A server-confirmed response is a valid unlock. The runtime injects the returned license payload into the browser. If the runtime cannot confirm the key with the server, it can fall back to local license metadata. That fallback records the key type and lets the session continue, but it is not the same as a server-confirmed capability unlock. Invalid or expired classifications are blocking failures and stop readiness before finalization. Feature gating comes from the generated method metadata. Each method declares a license tier of `none`, `insiders`, or `restricted`. Use that metadata as the feature list for your build. ## Exact gated features [#exact-gated-features] The current generated method schema marks these methods as licensed: | Feature | License tier | | -------------------------------------------------------------- | ------------ | | `getNumberProfile` | `insiders` | | `checkNumberStatus` | `insiders` | | `getOrder` | `insiders` | | `postTextStatus` | `insiders` | | `postImageStatus` | `insiders` | | `postVideoStatus` | `insiders` | | `getStories` / `getStatuses` | `insiders` | | `getStatus` | `insiders` | | `deleteStatus` | `insiders` | | `deleteAllStatus` / `deleteAllStatuses` | `insiders` | | `sendInteractive` (buttons, lists, forms, carousels, bookings) | `insiders` | | `sendRawMessage` (constructed protocol payloads) | `insiders` | | `sendButtons` | `insiders` | | `sendAdvancedButtons` | `insiders` | | `sendListMessage` / `sendList` | `insiders` | | `getStarredMessages` | `insiders` | | `setProfilePicture` / `setProfilePic` | `insiders` | | `createGroup` | `restricted` | | `joinGroupViaLink` / `joinGroup` | `restricted` | Methods marked `none` are not license-gated in the schema. Old docs and changelog entries can contain previous tiers, names, or runtime surfaces. Use the generated API reference for the package version that you deploy. For new v5 integrations, use [`sendInteractive`](/docs/guides/interactive-messages) for buttons, lists, forms, carousels, and bookings. It requires an applied Insiders or higher license for the sending account. The legacy `sendButtons`, `sendAdvancedButtons`, and `sendListMessage` methods remain in the schema with their own Insider gates; `sendButtons` and `sendListMessage` are deprecated. ### Runtime-specific number check [#runtime-specific-number-check] The ordinary `sendText` method is marked `none` in the schema, but the current client reports that sending to a number that is not already a contact requires a Restricted or Premium licence. This is an additional runtime check for that recipient path, so the method badge alone does not describe it. Confirm the runtime response and test that path in the session and release you will use. ## How keys are supplied [#how-keys-are-supplied] You can provide a license key in the runtime config: ```ts await create({ licenseKey: 'YOUR-LICENSE-KEY', }); ``` Or via the Easy API / CLI path: ```bash npx @open-wa/wa-automate@5.1.0 --session-id sales --host 127.0.0.1 --port 8080 --license-key "YOUR-LICENSE-KEY" ``` ## License behavior [#license-behavior] When the license is valid, the runtime applies the server-confirmed license payload, records the license lifecycle as satisfied, and continues launch. The relevant runtime status is `valid`. When the runtime can use only local metadata, it records `metadata_only`. The session can continue, but this status does not confirm an unlock. The license server did not confirm the capability. When you supply no key, the runtime records `missing`. Ungated methods continue to work. Do not expect gated methods to work. For an expired or invalid key, the runtime records `expired` or `invalid`. It marks the license lifecycle as failed and emits a fatal bootstrap error. Readiness stops before finalization. ## Common license workflow [#common-license-workflow] 1. review the current first-party offer and confirm it covers the behavior you intend to run 2. supply it through config or CLI at launch 3. verify the runtime recognizes the key in the session you care about 4. test the specific gated feature instead of assuming success from configuration alone ## Status inspection [#status-inspection] For Easy API sessions, inspect the `/health` endpoint and read the `license` object: ```bash curl http://localhost:8080/health ``` The response includes fields like: ```json { "license": { "status": "valid", "source": "local", "keyType": "server", "detail": "License capability remained server-confirmed at check time." } } ``` From a client method surface, you can also call `getLicenseType` to inspect the runtime license type exposed by the session: ```ts const licenseType = await client.getLicenseType(); console.log(licenseType); ``` For launch-level diagnostics, listen for `license.check.after` or inspect the launch timeline in `/health`. The same status values are used there: `valid`, `metadata_only`, `missing`, `invalid`, and `expired`. ## Operational advice [#operational-advice] * Treat license configuration as deploy-time secret material. * Validate the license against the actual host account you intend to run. * If unlock behavior looks inconsistent, verify the host number, deployment clock, and current runtime mode before assuming the key itself is wrong. ## If the key looks wrong [#if-the-key-looks-wrong] Check these first: * the host account number associated with the running session * whether the feature is available in the runtime surface that you use * whether the machine clock and deployment environment are sane * whether you are testing the gated feature through the same session the key is meant for ## Failure messages [#failure-messages] The current runtime uses these license failure details: | Situation | Message | | ------------------------------------------------ | ------------------------------------------------------------ | | Validation server rejects the key | `License key was rejected by the validation server.` | | Preload fails before validation | `License preload failed before validation.` | | No key is available | `No license material was available for this session.` | | Key is marked expired | `Resolved license key is marked as expired.` | | Key is marked invalid | `Resolved license key is marked as invalid.` | | Server-confirmed payload cannot be applied | `License payload did not confirm successful application.` | | Apply phase fails without a more specific detail | `License lifecycle did not complete successfully` | | Blocking status reaches launch finalization | `License lifecycle blocked readiness with status: ${status}` | | License validation request fails | `License validation request failed: ${error}` | You can also see the `license_server_validation_failed` warning in logs when the validation request fails and the runtime tries the metadata fallback path. ## Purchase link [#purchase-link] Use **Get a license** to choose Insiders or Restricted and enter your GitHub username, WhatsApp number, and use case. The purchase button on a licensed method selects that method’s tier for you. When you open [the license form](/checkout) from the dashboard, it fills in the session’s phone number when available and includes the session reference with your use case. Continue to Gumroad to review the selected license, prefilled details, current price, and terms before paying. You still need to configure the purchased key in your runtime. ## Support escalation [#support-escalation] If a license still fails, examine the host account, runtime version, deployment clock, and gated method. Then, collect the `/health` license object. Also collect the masked log key, host account number, and method name. For support, use the project Discord or the paid support links from the primary support page. Include the status value and failure message above so the issue can be routed quickly, and mask the key and host account number before sharing logs. ## Scope note [#scope-note] Feature availability has changed over time. Use the current generated API reference and runtime behavior as the source of truth for what is available in the build you are actually deploying. # Pricing and licensing (/docs/licensing/pricing) # Pricing and licensing [#pricing-and-licensing] Use this page to decide whether your workflow needs a runtime license and where to choose the right license for it. If you already know the gated method or runtime behavior you need, read **[Licensed features](/docs/licensing/licensed-features)** for the operational details before you buy. Configure your license here, then continue to Gumroad with your details already filled in. Review the current price and terms there before paying.
## First decision: do you need a key? [#first-decision-do-you-need-a-key] Start from the behavior your workflow needs. Method metadata marks methods as `restricted`, `insiders`, or `none`; runtime-specific checks can add a requirement to a particular path, and neither metadata nor a license makes an unsupported method available in your deployed version. ## Use-case and license matrix [#use-case-and-license-matrix] Use this matrix to separate runtime license requirements from upstream feature availability. A working host account and a method present in your deployed release are still required. The purchase buttons below select the relevant tier in the license form. ## Current gated methods [#current-gated-methods] The current licensed-feature page lists these gated methods. Treat this as a buying checklist, not a promise that every past or future build uses the same mapping. A license is part of the runtime launch path. Validate it with the host account, session, deployment clock, and exact gated method you plan to use in production. Enter that account’s WhatsApp number in the license form. If you open it from the dashboard, the number is filled in when available. ## What a license key changes at runtime [#what-a-license-key-changes-at-runtime] Supply the key through runtime config or the Easy API / CLI `--license-key` option. When the host account context is available, the runtime sends the key to the validation service. It classifies the result before it marks the client ready. A valid key is not a substitute for testing. Confirm the exact restricted or insiders method works in the same session and deployment you plan to run. ## Integration buyers [#integration-buyers] Integrations do not need a separate license just because they are integrations. The license decision follows the behavior the integration calls. ## Team and organization buying [#team-and-organization-buying] For team or organization questions, review the current offer at checkout and use the project support paths for terms it does not explain. The docs do not define multi-seat terms, per-developer scope, customer deployment scope, transfer rules, or support service levels. If the purchase has multiple users, accounts, environments, or customer deployments, do not assume its scope. If the current offer does not answer your purchase question, use Discord or the paid support links in the project README. ## Before you buy [#before-you-buy] ## Buy or verify first [#buy-or-verify-first]
Use the restricted path when your workflow depends on a method currently marked `restricted`, then validate the key in the session you will run. Use the insiders path when your workflow depends on a method currently marked `insiders`, then test that exact method before relying on it.
## Frequently asked questions [#frequently-asked-questions] If you are unsure, identify the exact method your workflow needs. If that still leaves a buying-scope question, use the purchase or support path instead of guessing. # Security and Deployment (/docs/operations/security-and-deployment) # Security and Deployment [#security-and-deployment] Running open-wa in production requires attention to API key management, network security, and session protection. The published npm CLI `5.1.0` defaults to `0.0.0.0:8002`, so use explicit `--host` and `--port` values for the intended bind address and port. ## Runtime prerequisites [#runtime-prerequisites] For monorepo development and source builds, the repo declares **Node.js >= 22.21.1** and pins `packageManager` to **pnpm\@11.15.1**. Match those versions before debugging runtime or build issues. ## API Key Management [#api-key-management] ### Why API Keys Are Necessary [#why-api-keys-are-necessary] Protect every open-wa session with an API key. Without a key, each user who can access your server can control the WhatsApp session. ### Generate random keys [#generate-random-keys] Use a cryptographically random string: ```bash # Generate a random key openssl rand -hex 32 # or node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ``` ### Storing Keys Safely [#storing-keys-safely] **Environment variables** are the recommended approach: ```bash export WA_API_KEY="your-generated-key" npx @open-wa/wa-automate@5.1.0 --host 127.0.0.1 --port 8080 --api-key "$WA_API_KEY" ``` Or in a `.env` file: ``` WA_API_KEY=your-generated-key ``` For production deployments, use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler, etc.). ### Rotating API keys [#rotating-api-keys] The API key is process configuration, so changing it requires a coordinated restart or deployment. Keep the session profile in place; changing the key alone does not require linking WhatsApp again. ### What Happens If a Key Is Leaked [#what-happens-if-a-key-is-leaked] If your API key is exposed: 1. Stop the running session immediately 2. Generate a new API key 3. Restart with the new key 4. Audit what actions were taken with the leaked key 5. Consider logging out from the phone and re-authenticating if you suspect unauthorized access ## Port and Network Security [#port-and-network-security] ### Exposing Ports to the Internet [#exposing-ports-to-the-internet] **Never expose the Easy API port directly to the internet without authentication.** `/health` is public even when the API key protects API calls, and its response can include QR and diagnostic data. Keep the health route private and do not expose it through a public proxy. At minimum: * Always use `--api-key "your-secure-key"` * Use a reverse proxy (nginx, Caddy) with HTTPS * Restrict access to known IP addresses when possible ### Use reverse proxies [#use-reverse-proxies] **nginx example:** ```nginx server { listen 443 ssl; server_name whatsapp-api.example.com; location / { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } ``` **Caddy example:** ``` whatsapp-api.example.com { reverse_proxy localhost:8080 } ``` ### Cloudflare Tunnel Alternative [#cloudflare-tunnel-alternative] Cloudflare Tunnel can route traffic to a local API without an inbound firewall port: ```bash cloudflared tunnel --url http://localhost:8080 ``` The tunnel creates an internet-facing route, so keep API authentication enabled and account for the public `/health` response described above. ### Firewall Rules [#firewall-rules] If you must expose the API directly, restrict access: ```bash # Only allow specific IPs ufw allow from 1.2.3.4 to any port 8080 ``` ## Webhook Security [#webhook-security] ### Securing Webhook Endpoints [#securing-webhook-endpoints] When open-wa sends webhooks to your service: * Use HTTPS for the webhook URL * Add authentication headers via the webhook `headers` config * Have your receiver reject requests without the expected shared-secret header The webhook integration sends configured headers; it does not provide signed-payload verification. See [Webhook payloads](/docs/client-and-integrations/webhook-payloads) for the supported plugin configuration. ### Rate Limiting Webhook Receivers [#rate-limiting-webhook-receivers] Your webhook receiver must handle the volume of incoming events. The webhook plugin supports: * `concurrency` (default: 10): max concurrent deliveries * `retries` (default: 3): retry attempts on failure * `retryDelay` (default: 1000ms): base delay with exponential backoff * `timeout` (default: 30000ms): request timeout ## MCP Security [#mcp-security] ### API Key Enforcement [#api-key-enforcement] MCP requires the Easy API's API key and will not start without one. Every MCP request must include that key through an accepted API-key header: `X-API-Key`, `api_key`, or `key`. ### Endpoint Exposure Risks [#endpoint-exposure-risks] If the MCP endpoint is exposed publicly, the API key is leaked, or untrusted agents are allowed to use the key, those agents could: * Read all messages in all chats * Send messages to any contact * Access session information Always protect the MCP endpoint with the API key, keep the key secret, and, ideally, place the endpoint behind a reverse proxy. ### Restricting AI Agent Operations [#restricting-ai-agent-operations] Currently, MCP exposes all Easy API methods as tools. There are no permission scopes, so any authenticated agent can call any method. Plan your deployment accordingly. ### Audit Trails [#audit-trails] Monitor MCP usage through: * API access logs * The dashboard MCP page (`http://localhost:8080/dashboard/mcp`) * Your reverse proxy access logs ## Proxy Security [#proxy-security] ### Cloudflare Proxy Token Rotation [#cloudflare-proxy-token-rotation] The Cloudflare Session Proxy uses consumer tokens for authentication. Replace these tokens periodically: 1. Update the token in your Cloudflare Worker 2. Update the token in your consumer connection string 3. Test the new connection before removing the old token ### Custom Domains [#custom-domains] You can attach a custom domain to your Cloudflare Worker for a cleaner connection URL. ## Production Checklist [#production-checklist] Before deploying to production: * [ ] API key set and stored securely (not in code or config files committed to git) * [ ] HTTPS enabled (reverse proxy or Cloudflare Tunnel) * [ ] Session persistence configured (volumes for Docker, persistent disk for servers) * [ ] Webhook auth configured (headers or IP restriction) * [ ] Rate limits understood and safe defaults applied * [ ] Logging enabled and monitored * [ ] Backup strategy for session files in place * [ ] Monitoring configured (health checks, alerting on session disconnect) * [ ] Process manager configured (PM2, systemd, Docker restart policy) * [ ] Firewall rules applied (only necessary ports open) ## Deployment Topologies [#deployment-topologies] ### Single Server [#single-server] ``` [Your App] ←→ [open-wa Easy API] ←→ WhatsApp Web (browser) ``` * Simplest setup * Run with `npx @open-wa/wa-automate@5.1.0 --host 127.0.0.1 --port 8080 --api-key "key"` * Use PM2 or systemd for process management ### Docker Container [#docker-container] Use the [Docker baseline](/docs/getting-started/docker), which pins the inspected image digest, configures a persistent profile and key, and passes the published CLI's host, port, and session flags explicitly. Keep the container port private because `/health` remains public and can include QR and operational details. ### Cloudflare Proxy + Local Runtime [#cloudflare-proxy--local-runtime] ``` [Your App] ←→ [Cloudflare Proxy] ←→ [open-wa Easy API (local)] ←→ WhatsApp ``` * No open ports on your server * Remote access without public exposure * See **[Cloudflare Session Proxy](/docs/client-and-integrations/cf-proxy)** ### Kubernetes [#kubernetes] For large-scale deployments: * One pod per session (sessions do not share state) * Persistent volumes for session data * Horizontal Pod Autoscaler based on session count * Service mesh for internal routing ## Related [#related] * **[Quick Start](/docs/getting-started/quickstart)**: First-time setup * **[Docker](/docs/getting-started/docker)**: Pinned image and persistent session setup * **[Cloudflare Session Proxy](/docs/client-and-integrations/cf-proxy)**: Protected remote access * **[Webhook payloads](/docs/client-and-integrations/webhook-payloads)**: Webhook plugin configuration * **[Best Practices](/docs/operations-and-troubleshooting/best-practices)**: Production patterns # Refresh a Live Session (/docs/operations/session-refresh) # Refresh a live session [#refresh-a-live-session] Choose **Refresh patches** on the dashboard's Health page to download the public patches again and rebuild the WhatsApp page. The browser process, Node process, API server, client object, host listeners, and session persistence lease stay alive. **Refresh status** only reloads the health information displayed in the dashboard. **Warning:** Refreshing reloads WhatsApp. You may miss messages during the refresh. This also applies when entering a license key triggers a refresh. Refresh rebuilds the page even when the downloaded patch tag hasn't changed. A page reload temporarily interrupts WhatsApp operations and ends document-owned call media; attach fresh media after the session is ready. Refresh does not pause application calls, native message storage, or receipts, and does not wait for pending operations before reloading. Operations interrupted by the page reload aren't replayed automatically. ## Apply a license during a session [#apply-a-license-during-a-session] Open the dashboard's license badge, enter your key, and choose **Apply license**. OpenWA downloads fresh public patches and reloads the page in the background. It then requests the usual license code for the connected account, evaluates it, initializes the runtime, and restores its event bridge. A failed public patch download leaves the current page running. If the license code rejects the key, the dashboard reports the rejection and the refreshed session remains ready with public functionality. It doesn't restart the host or restore the entire page for a license rejection. The submitted key becomes the effective session key only after its code confirms successful application. Future refreshes and document recovery use that key to request fresh licensed material. A key entered in the dashboard lives in host session memory; configure `licenseKey` or `--license-key` separately if it should survive a process restart. The form clears the key after the server accepts the operation. Operation status and progress events contain no license key. License metadata alone doesn't mean licensed functionality was installed. ## Use the client [#use-the-client] The in-process `Client`, HTTP/SSE `SocketClient`, and `TunnelSocketClient` expose the same controls: ```ts const result = await client.refresh(); // Or activate a key through the same operation: const activation = await client.refresh({ licenseKey: process.env.WA_LICENSE_KEY }); const status = await client.getRefreshStatus(); ``` To accept the work without waiting for installation: ```ts const { operationId } = await client.requestRefresh(); const status = await client.getRefreshStatus(); ``` Inspect `phase`, `running`, `runtimeUsable`, `restored`, and `error`. `phase: 'ready'` means the replacement completed. A license rejection reports `phase: 'failed'` with `SESSION_REFRESH_LICENSE_REJECTED` and `runtimeUsable: true`; the session stays available. `phase: 'failed', restored: true` means a runtime installation failed but the previous public patches were restored. `runtimeUsable: false` means the document runtime is unavailable. If `phase` is `needs_auth`, pair the session using its existing QR journey. The accepted operation retains ownership while waiting and resumes installation after pairing. Read status again to follow that continuation; `refresh()` can return the actionable pairing state before the background operation finishes. ## Use the HTTP controls [#use-the-http-controls] Both routes use the configured API key policy and remain available while WAPI is being restored. Supply `X-API-Key` when an API key is configured. | Method | Route | Body | Result | | ------ | ---------------------- | -------------------------------------- | ---------------------------- | | `POST` | `/api/session/refresh` | `{}` or `{ "licenseKey": "YOUR_KEY" }` | `202 { "operationId": "…" }` | | `GET` | `/api/session/refresh` | None | Current operation snapshot | Acceptance isn't installation success. Follow the status route or `session.refresh.progress` on the existing event stream. Closing the requesting dashboard or client doesn't cancel the accepted server operation. Repeated requests with the same intent return the current operation ID. A conflicting key receives HTTP 409, invalid input receives 400, and an unavailable runtime owner receives 503. ## When refresh fails [#when-refresh-fails] A patch download failure leaves the page running. If runtime installation fails after the page has been replaced, OpenWA attempts one restoration using the previously installed public patches and the previous effective key, requesting fresh license code. Failed restoration reports the runtime as unavailable while retaining the browser and session data. A license rejection alone doesn't trigger restoration. ## License endpoint and browser source [#license-endpoint-and-browser-source] Licensed refresh uses the existing `/license-check` request and executable response. The response's own account and expiry guards decide whether licensed functionality can be installed. No preparation flag or additional license-service deployment is required. The generated pre-init asset comes from the private portable-session source. Refresh uses its normal persistence behavior without a separate preparation or pause step. Generate it from the private source; don't modify the generated asset or immutable `wapi.js` by hand. # Best practices (/docs/operations-and-troubleshooting/best-practices) # Best practices [#best-practices] Keep one runtime responsible for each session, persist its profile, and move slow business work into a queue or separate worker. The sections below cover the operational choices that affect recovery, security, and message delivery. ## Production checklist [#production-checklist] Before deploying a session that users depend on: * [ ] API key set and stored outside committed files. * [ ] Session persistence configured on durable disk or a mounted Docker volume. * [ ] Error handling in place for startup, logout, browser, transport, and send failures. * [ ] Monitoring and alerting configured for disconnects, repeated send failures, and process crashes. * [ ] Restart policy configured with PM2, systemd, Docker, Kubernetes, or another supervisor. * [ ] Rate limits respected with queues, delays, and backoff. * [ ] Backups in place for session data and deployment config. ## Security checklist [#security-checklist] For production systems, treat the open-wa runtime like any other system that can send messages on behalf of your business: * [ ] HTTPS enabled for all public endpoints. * [ ] API key rotated regularly and changed immediately after suspected exposure. * [ ] Webhook secrets or authentication headers verified by your receiver. * [ ] No secrets stored in config files that are committed to git. * [ ] Firewall rules restrict access to only the ports and clients you need. ## Rate limits [#rate-limits] WhatsApp does not publish fixed automation limits. Production systems must expect limit changes without notice. Send messages slowly. Do not send identical bulk messages. Add backoff after failures. Prefer automation that replies to received messages. See **[Rate Limits and Ban Risk](/docs/guides/rate-limits)** for safe timing defaults, high-risk patterns, and mitigation steps. ## Queues [#queues] Queues keep high message volume from turning into uncontrolled callback fan-out. They let you cap concurrency, add delays, retry failed work, and drain unread messages after downtime without sending everything at once. Use queues when: * incoming messages can arrive faster than your app can process them * sending needs strict spacing to reduce rate-limit and ban risk * work depends on slow services such as CRMs, databases, or LLM APIs * failed jobs need retry or dead-letter handling ## Example queue-based pattern [#example-queue-based-pattern] ```ts import PQueue from 'p-queue'; const queue = new PQueue({ concurrency: 4, autoStart: false }); async function start(client: any) { const unreadMessages = await client.getAllUnreadMessages(); unreadMessages.forEach((message: any) => queue.add(() => processMessage(message))); await client.onMessage((message: any) => queue.add(() => processMessage(message))); queue.start(); } ``` Use this pattern when throughput matters more than naive callback fan-out. ## Observability [#observability] Good observability makes session problems visible before users report them. * **Logging**: Log startup, QR or link-code auth, session readiness, disconnects, reconnects, webhook delivery failures, and send failures. Include `sessionId`, chat id, message id, and error type where safe. * **Metrics**: Track connected sessions, inbound message count, outbound message count, queue depth, queue latency, send failures, webhook failures, and process restarts. * **Tracing**: Add request or job ids across webhooks, queues, business logic, and send calls so one incoming message can be followed through the whole system. Keep message bodies, media, phone numbers, and credentials out of logs unless you have a clear privacy and retention policy. ## Backups [#backups] Back up the data needed to recover a session without rebuilding the deployment from memory. Back up: * session data and auth state * runtime config, without plain-text secrets * deployment manifests such as Docker Compose, systemd units, or Kubernetes YAML * webhook and integration config For a single server, schedule filesystem snapshots or a daily archive of the session directory. For Docker, mount session data to a named volume or host path, then back up that volume. For distributed deployments, use persistent volumes and test restore into a staging environment before trusting the backup plan. Do not commit raw session data or API keys to git. ## Deployment topology [#deployment-topology] Common production shapes: * **Single server**: open-wa, your app, and the process supervisor run on one machine. This is simplest and works well for one or a few sessions. * **Docker**: open-wa runs in a container with a mounted session volume and a restart policy. This gives predictable runtime packaging and easier rollbacks. * **Distributed**: open-wa runs as a dedicated runtime service, while web apps, workers, queues, and databases run separately. This is better for larger systems and multi-session deployments. Keep one session's persistent data isolated from another session. If you run multiple sessions, give each one a stable `sessionId`, port or route, API key, and storage path. ## Scaling [#scaling] Scale the system around the session boundary. * **Vertical scaling**: Increase CPU, memory, and browser resources for a busy session. This helps when one account receives many events or uses heavy browser operations. * **Horizontal scaling**: Add more open-wa sessions or workers. This helps when you have many accounts, many tenants, or business logic that can be processed outside the WhatsApp runtime. Do not run the same WhatsApp session from multiple runtimes against the same session data. Instead, keep one runtime active per session, then scale consumers, queues, and downstream workers around it. ## AI agent patterns [#ai-agent-patterns] AI agents need stricter guardrails than simple bots because they can generate variable output and call tools repeatedly. Use safe defaults: * respond only to messages that must start the agent * skip your own messages, status updates, system messages, and unsupported media * rate-limit all outbound replies * keep a bounded conversation context window * log tool calls and agent decisions without storing sensitive message content unnecessarily See **[AI Agent Patterns](/docs/guides/ai-agent-patterns)** for conversation loops, context management, rate limiting, and production safety patterns. # Detect logouts (/docs/operations-and-troubleshooting/detect-logouts) # Detect logouts [#detect-logouts] Use `onStateChanged` to watch session state and react when the session becomes conflicted, unlaunched, or unpaired. ```ts client.onStateChanged((state) => { console.log('statechanged', state); if (state === 'CONFLICT' || state === 'UNLAUNCHED') { client.forceRefocus(); } if (state === 'UNPAIRED') { console.log('The session has logged out'); } }); ``` ## Full state enum [#full-state-enum] Use a small session state enum in your own monitoring code so every runtime event lands in one clear bucket. | State | Description | | --------------- | ----------------------------------------------------------------------------------------------------------- | | `starting` | The runtime is launching the browser, loading session data, and preparing WhatsApp Web. | | `qr` | The session needs a QR scan before it can continue. This usually means no valid saved session was restored. | | `authenticated` | WhatsApp accepted the saved session or QR scan, but the client is not fully ready yet. | | `ready` | The client is connected and safe for normal API calls. | | `disconnected` | The connection dropped after startup. This can be temporary, or it can mean the account was logged out. | | `stopping` | The process is shutting the client down on purpose. Do not treat this as a logout. | | `stopped` | The client has finished shutting down. A new process start is necessary before it can connect again. | The older `onStateChanged` values such as `CONFLICT`, `UNLAUNCHED`, and `UNPAIRED` are still useful signals. Map them into your own state enum so alerts and recovery code do not depend on every raw value. ## State transitions [#state-transitions] These are the normal transitions to expect in production monitoring: | From | To | Trigger | | --------------- | --------------- | ------------------------------------------------------------------------ | | `stopped` | `starting` | The process starts, or your supervisor restarts it. | | `starting` | `authenticated` | A saved session is restored, or a QR scan completes. | | `starting` | `qr` | No usable saved session is available, or the saved session was rejected. | | `qr` | `authenticated` | The operator scans the QR code and WhatsApp accepts the login. | | `authenticated` | `ready` | Client finalization completes and `client.ready` fires. | | `ready` | `disconnected` | The browser, network, phone, or WhatsApp Web connection drops. | | `disconnected` | `ready` | The runtime reconnects without needing a new QR scan. | | `disconnected` | `qr` | The session can no longer be restored and must be paired again. | | `starting` | `stopping` | Startup is cancelled or the process receives a shutdown signal. | | `ready` | `stopping` | The app intentionally calls shutdown, exits, or receives a signal. | | `disconnected` | `stopping` | The app gives up recovery and shuts down cleanly. | | `stopping` | `stopped` | Cleanup finishes. | Log all other transitions. A valid transition can still occur, but the log entry must include the raw state, previous state, reason, and session ID. ## Recovery recipe [#recovery-recipe] When a session logs out or disconnects, recover in this order: 1. Record the transition with `sessionId`, previous state, next state, reason, and timestamp. 2. Pause outbound sends for that session so your queue does not keep retrying against a dead client. 3. Check whether the process is intentionally `stopping` or `stopped`. If it is, let your supervisor restart it if that is your normal deployment model. 4. If the state is `disconnected`, wait for a short backoff period. Permit one reconnect attempt from the same saved session. 5. If the session returns to `ready`, drain the paused queue carefully and mark the incident as recovered. 6. If the state moves to `qr` or `UNPAIRED`, stop automatic sends, alert an operator, and start the QR re-auth workflow. 7. If reconnect attempts occur again, keep the session offline. Do a manual check before you send more messages. Keep the recovery loop small. Use one immediate reconnect window and one delayed restart. Then, send an alert if the session cannot get to `ready`. ## QR re-auth [#qr-re-auth] Session restoration works when the saved browser profile and WhatsApp Web session are still accepted by WhatsApp. In that case, the runtime can move from `starting` to `authenticated` and then `ready` without a new scan. QR re-authentication is needed when the saved session is missing, expired, rejected, or explicitly logged out from the phone or another WhatsApp Web device. You will usually see `qr`, `UNPAIRED`, or a repeated failure to leave `starting`. Do not delete session data as the first response to every disconnect. A normal network drop can often recover from the existing session. Delete or replace session data only after you have confirmed the saved session is no longer accepted. ## Alerting example [#alerting-example] Alert on state changes, not only on process exits. This catches a session that is alive as a Node.js process but no longer paired with WhatsApp. ```ts const lastStateBySession = new Map(); function reportSessionState(sessionId: string, nextState: string, reason?: string) { const previousState = lastStateBySession.get(sessionId) ?? 'unknown'; lastStateBySession.set(sessionId, nextState); console.log('session_state_changed', { sessionId, previousState, nextState, reason, timestamp: new Date().toISOString(), }); if (nextState === 'qr' || nextState === 'disconnected') { notifyOps({ title: `WhatsApp session ${nextState}`, message: `${sessionId} moved from ${previousState} to ${nextState}`, reason, }); } } client.onStateChanged((state) => { const nextState = state === 'UNPAIRED' ? 'qr' : state.toLowerCase(); reportSessionState('sales', nextState, state); }); ``` Send the same state event to your metrics system if you have one. Useful counters are `session_state_change_total`, `session_disconnect_total`, and `session_reauth_required_total`. ## What not to auto-retry [#what-not-to-auto-retry] Automatic retry is useful for short network drops. It is harmful when the account or session needs a human decision. * Do not keep sending messages while the session is `qr`, `UNPAIRED`, or repeatedly failing authentication. * Do not loop QR generation forever. Alert someone who can scan it. * Do not delete and recreate session data on every `disconnected` event. That can turn a recoverable network issue into a forced login. * Do not retry high-volume outbound queues after repeated disconnects. This can increase ban risk and make the account look abusive. * Do not keep restarting the browser if WhatsApp rejects the saved session multiple times. Require manual review. * Do not hide repeated auth failures behind a supervisor restart loop. Surface them as an incident. # Error handling (/docs/operations-and-troubleshooting/error-handling) # Error handling [#error-handling] Treat every client method as an async operation and handle failures explicitly. ## Embedded runtime pattern [#embedded-runtime-pattern] ```ts import { Client, createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; const runtime = await createClient({ sessionId: 'sales', driver: new PuppeteerDriver(), headless: false, }); const client = new Client({ client: runtime, transport: runtime.getTransport() }); await client.start(); runtime.events.on('message.received', async ({ message }) => { try { await client.sendText(message.from, 'Hi!'); } catch (error) { console.error('sendText failed', error); } }); ``` `createClient()` creates the embedded runtime and `client.start()` begins the browser/session lifecycle. Keep those ownership boundaries together; use `SocketClient` when another Easy API process owns the browser. ## Handling startup failures [#handling-startup-failures] ```ts async function launch() { try { const runtime = await createClient({ sessionId: 'sales', driver: new PuppeteerDriver(), headless: false, }); const client = new Client({ client: runtime, transport: runtime.getTransport() }); await client.start(); const readiness = runtime.getReadiness(); if (!readiness.ready) throw new Error(`Session is not ready: ${readiness.pending.join(', ') || 'unknown blocker'}`); return { runtime, client }; } catch (error) { console.error('open-wa failed to start', error); throw error; } } ``` ## Operational helpers [#operational-helpers] * `restartOnCrash` can help supervised runtimes restart predictably. * `logConsole` and `logConsoleErrors` help when the browser runtime is the real failure source. * `killProcessOnBrowserClose` is useful when an external supervisor is responsible for restart policy. ## Browser/page errors [#browserpage-errors] Because the runtime is browser-backed, you can also inspect page-level errors through the underlying page surface when your flow needs deeper debugging. ## Common error catalogue [#common-error-catalogue] Use the catalogue below to separate startup problems, API problems, and WhatsApp-side risk signals before you retry or restart. | Error | Common code or signal | Likely cause | What to do | | ---------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Auth timeout | `AUTH_TIMEOUT`, `qrTimeout`, `authTimeout`, repeated QR expiry | The QR was not scanned in time, the link code expired, the phone was offline, or the session token is no longer valid. | Restart the session, keep the phone online, increase the timeout if humans need more time, and delete the session files only when the saved session is clearly invalid. | | Browser launch failure | `BROWSER_LAUNCH_FAILED`, `ENOENT`, `EACCES`, sandbox errors, missing shared libraries | Chrome or Chromium cannot start, the configured `executablePath` is wrong, the container is missing browser dependencies, or the runtime lacks permissions. | Confirm the browser path, test with `headless: false`, install missing system packages, and use container flags that match your hosting environment. | | Blocked account risk warning | Login loops, forced logout, unusual activity prompts, send failures after high volume | WhatsApp can limit or review the account because of automation patterns, message volume, recipient quality, or device changes. | Stop automation and review recent traffic. Decrease the send rate and reconnect from the phone. Do not delete sessions while the account is restricted. | | API authentication error | HTTP `401` | Missing or wrong Easy API key. | Send the configured key with `X-API-Key` or the SocketClient connection key. Replace the key if it was exposed. | | API forbidden error | HTTP `403` | The request is authenticated but not allowed for the current API surface, session state, or deployment boundary. | Check the endpoint, method, API key scope if applicable, and whether the session is ready. | | API rate limit error | HTTP `429` | Too many HTTP calls or too much message activity in a short period. | Back off, add jitter, queue work, and lower concurrency. Do not retry immediately in a tight loop. | | API server error | HTTP `500` | The runtime failed while handling the request, often because the browser, session, or WhatsApp Web state changed underneath it. | Capture logs, check session readiness, retry once after a short delay, then restart the runtime if the session is stuck. | ## Auth timeout [#auth-timeout] Auth timeouts happen before the session becomes ready. The runtime is waiting for a QR scan, link-code confirmation, or saved session reconnect, and the expected state does not arrive before the configured timeout. Common causes: * The QR code was not scanned before `qrTimeout` expired. * The link code expired before it was entered on the phone. * The phone running WhatsApp is offline or has a weak connection. * The saved session files are missing, stale, or corrupted. * The account was logged out from WhatsApp > Linked Devices. Recovery steps: 1. Restart the process and watch for a new QR code or link code. 2. Keep the phone online and open WhatsApp while the runtime authenticates. 3. Increase `qrTimeout` or `authTimeout` when a human operator needs more time. 4. If the same saved session continues to fail, log out that linked device from the phone. Delete only that session's files, and authenticate again. ```ts const runtime = await createClient({ driver: new PuppeteerDriver(), sessionId: 'sales', qrTimeoutMs: 0, authTimeoutMs: 120_000, }); const client = new Client({ client: runtime, transport: runtime.getTransport() }); await client.start(); ``` Use `qrTimeoutMs: 0` only when you want the QR flow to wait without expiring in unattended or slow manual setup. For production, pair long auth windows with process supervision so a stuck startup can still be restarted intentionally. ## Browser failures [#browser-failures] Browser failures usually happen during `createClient()` or shortly after `client.start()`. The Node.js process is running, but the browser driver cannot launch, attach, navigate, or keep the WhatsApp Web page alive. Check these first: * Confirm Chrome or Chromium is installed if you set `useChrome` or `executablePath`. * Remove a custom `executablePath` temporarily and let the selected driver use its default browser. * Run once with `headless: false` in a safe desktop environment so you can see the browser state. * In Linux containers, install the browser's necessary shared libraries and check sandbox permissions. * Make sure only one process owns the same session directory at a time. * Enable browser console logging when the page loads but WhatsApp Web fails inside the browser. ```ts const runtime = await createClient({ driver: new PuppeteerDriver(), sessionId: 'sales', useChrome: true, headless: false, logConsole: true, logConsoleErrors: true, }); ``` If the browser closes after startup, treat it as a runtime failure rather than an auth failure. Restart the process with the same `sessionId` first. The saved session can usually reconnect without another QR scan unless WhatsApp invalidated it. ### `Navigating frame was detached` / `Attempted to use detached Frame` [#navigating-frame-was-detached--attempted-to-use-detached-frame] These specific errors, especially as a crash loop shortly after the page loads, almost always mean one of: 1. **`--single-process` or `--no-zygote` in `chromiumArgs`.** These crash modern Chrome on WhatsApp Web. The runtime strips them by default and logs `Removed browser args known to crash WhatsApp Web`; if you forced them with `allowDangerousBrowserArgs: true`, remove that. This is the most common cause in low-RAM Docker/Railway deployments. 2. **The renderer was OOM-killed.** Check container memory limits and host `dmesg` / container OOM events. See the low-memory guidance on the Docker page. 3. **Too-small `/dev/shm`.** Give the container real shared memory (`--shm-size=1gb`) or keep `--disable-dev-shm-usage`. Do not respond to detached-frame errors by adding more Chromium flags — the fix is almost always removing flags and giving the browser enough memory. ## Blocked account risk [#blocked-account-risk] Some failures come from WhatsApp behavior rather than the open-wa process. Treat these as account risk signals, not bugs to retry through. Signs include: * WhatsApp Web repeatedly logs out a valid session. * The phone shows unusual activity, spam, or verification prompts. * Sends begin failing after a burst of outbound messages. * New chats or unknown recipients fail more often than existing conversations. * The same account works on the phone but becomes unstable as soon as automation resumes. What to do: 1. Stop outbound automation for that account. 2. Check the phone for warnings or necessary action. 3. Reduce send volume, concurrency, and repeated identical message patterns. 4. Prefer replies and expected conversations over cold outbound sends. 5. Keep the same device, session, and network stable where possible. 6. Resume slowly only after the account behaves normally in the official app. Do not try to solve account restriction by deleting session files in a loop. That can create more linked-device churn and make the account look less stable. ## API HTTP errors [#api-http-errors] Easy API errors are HTTP responses from the local or remote API surface. They tell you whether the request reached the API and how the API classified the failure. | Status | Meaning | Common fix | | --------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | `401 Unauthorized` | The request did not include a valid API key. | Pass the key as `X-API-Key` or through `SocketClient.connect(baseUrl, apiKey)`. | | `403 Forbidden` | The API understood the request, but the current caller or runtime state is not allowed to do it. | Check the endpoint, deployment boundary, session readiness, and whether the method is exposed through that surface. | | `429 Too Many Requests` | The caller is sending too many requests or hitting a configured limit. | Back off with jitter, lower concurrency, and queue work per session. | | `500 Internal Server Error` | The API hit an unexpected runtime failure while handling the request. | Check logs, session state, browser health, and retry only after a short delay. | For SocketClient consumers, handle command failures like normal async errors: ```ts try { await client.sendText(chatId, 'Hello'); } catch (error) { console.error('Easy API command failed', error); } ``` ## Readiness and authentication checks [#readiness-and-authentication-checks] Run these checks from the same host that calls Easy API. `/health` is public: it reports process and session readiness but does not check the API key. The metadata request checks a configured key, so the two results separate an unreachable process, a rejected key, and a live process whose WhatsApp session is still authenticating: ```bash export OPENWA_API_URL="http://127.0.0.1:8080" export WA_API_KEY="your-secure-key" curl -sS -D - \ "$OPENWA_API_URL/health" curl -sS -D - \ -H "X-API-Key: $WA_API_KEY" \ "$OPENWA_API_URL/meta/basic/commands" ``` Interpret the response before retrying a command: * `HTTP 200` from `/health` with `connected: true` and `session.ready: true` means the API and WhatsApp session are ready. A successful `/meta/basic/commands` response confirms that the same URL and configured key can load command metadata. * `HTTP 200` with `connected: false` or `session.ready: false` means the process is alive but authentication or runtime finalization is incomplete. Finish the QR or link-code flow, then inspect `session.pending` and `session.blockers` and check again. * `HTTP 401` from `/meta/basic/commands` means the key is absent or does not match the process configuration. `/health` does not return `401` for a missing or incorrect key. Compare the value passed as `--api-key` (or its config source) with `WA_API_KEY`; do not keep retrying the same key. * A connection-refused or timeout error means the host, port, process, or network boundary is wrong. Start Easy API or correct `OPENWA_API_URL` before investigating WhatsApp state. Once readiness is true, make one low-risk send request and keep the response headers while diagnosing delivery: ```bash curl -sS -D - -X POST \ -H "X-API-Key: $WA_API_KEY" \ -H "Content-Type: application/json" \ "$OPENWA_API_URL/api/messages/sendText" \ -d '{"to":"447123456789@c.us","content":"readiness check","options":{}}' ``` An HTTP `2xx` response with a message result means the API accepted the request. A `401` sends you back to key configuration, a `503` means the API middleware is still waiting for a ready session, and a `4xx` response usually means the recipient ID or JSON body needs correcting. Do not turn a `200` health response into a send attempt until both readiness fields are true. ## Retries [#retries] Retry based on the error type. A retry strategy that helps one class of failure can make another class worse. | Error type | Retry strategy | | ------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Auth timeout | Restart the auth flow after checking phone availability. Do not run many parallel auth attempts for the same account. | | Browser launch failure | Do not retry blindly. Fix the browser path, dependencies, permissions, or container flags first. | | Browser crash after ready | Restart the runtime with the same `sessionId`. Let the saved session reconnect before deleting anything. | | HTTP `401` or `403` | Do not retry until configuration is fixed. These are usually key, endpoint, or permission problems. | | HTTP `429` | Retry with exponential backoff and jitter. Lower concurrency before resuming normal traffic. | | HTTP `500` | Retry once or twice with a short delay. If it repeats, inspect logs and restart the session. | | Blocked account risk | Do not retry automatically. Stop automation and check the account from the phone. | Example retry wrapper for transient API failures: ```ts async function retryTransient(task: () => Promise) { const delays = [1000, 3000, 8000]; for (const delay of delays) { try { await task(); return; } catch (error) { console.error('Transient command failure', error); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error('Command failed after retries'); } ``` Keep retries idempotent where possible. Record the message intent before you send a message. This record prevents duplicate sends after a timeout. ## Logging config [#logging-config] Turn on enough logging to see which layer failed: your app, Easy API, the browser driver, or WhatsApp Web inside the browser. For Easy API, start by capturing stdout and stderr: ```bash npx @open-wa/wa-automate@5.1.0 --session-id sales --host 127.0.0.1 --port 8080 --log-console --verbose > open-wa.log 2>&1 ``` For custom code, enable browser console logging during diagnosis: ```ts const runtime = await createClient({ driver: new PuppeteerDriver(), sessionId: 'sales', logConsole: true, logConsoleErrors: true, }); ``` Log these fields around failed commands: * session id * method name * request id or job id from your app * chat id or recipient id when safe to store * HTTP status code for Easy API calls * whether the session was ready before the call Do not log session tokens, API keys, complete message bodies, or personal data unless your compliance rules require it. ## Recovery playbooks [#recovery-playbooks] 1. Start the runtime with visible logging. 2. Confirm a QR code or link code is actually emitted. 3. Keep the phone online and complete the login flow. 4. If the saved session is reused and still fails, log out the linked device from the phone. 5. Delete only that session's files and authenticate again. 1. Remove custom browser config and try the default driver behavior. 2. Check `executablePath` if you require a specific Chrome install. 3. Run with `headless: false` on a desktop host to see launch errors. 4. Install missing Linux browser dependencies in containers. 5. Restart under your supervisor only after the browser can launch manually. 1. Confirm the Easy API was started with the expected `--api-key`. 2. Send the same key in the `X-API-Key` header. 3. If you use SocketClient, pass the key as the second argument to `SocketClient.connect()`. 4. Replace the key if it was copied into logs, browser code, or shared chat. 1. Stop immediate retries. 2. Queue new work for the affected session. 3. Resume with lower concurrency and jitter between jobs. 4. Watch for WhatsApp-level warnings before returning to normal send volume. 1. Stop sends from automation. 2. Open WhatsApp on the phone and resolve any prompts. 3. Review recent recipients, send rate, and repeated message patterns. 4. Let the account cool down before reconnecting automation. 5. Resume with safer pacing and monitor failures closely. ## Error type distinction [#error-type-distinction] Handle each failure type separately. * **Runtime errors** happen inside your Node.js process or the browser runtime. Examples include thrown client method errors, browser crashes, missing dependencies, and page errors. * **API errors** are HTTP responses from Easy API. They have status codes such as `401`, `403`, `429`, and `500`. The HTTP caller must handle them. * **WhatsApp-level errors** come from the connected account or WhatsApp Web behavior. Examples include logout, blocked sends, verification prompts, and account restriction warnings. Handle runtime errors with process supervision and clear logs. Handle API errors with caller-side status handling. Handle WhatsApp-level errors conservatively, because aggressive retries can make account risk worse. # AI Tools (/docs/plugins/ai-tools) # AI Tools [#ai-tools] Plugins can declare tools in the `tool` hook returned by `init()`. The current open-wa MCP endpoint does not discover or invoke plugin-declared tools, so use these examples only when you are building a host integration that connects the hook to an agent. The declaration alone does not make a tool available to an agent. Connect it to a tool host before advertising it. ## Declare a tool [#declare-a-tool] Each tool has a name, description, argument schema, and execution function. The host integration decides whether and how agents can discover and call it. ```ts init: async ({ client }) => ({ tool: { toolName: { description: 'What this tool does', args: { argName: z.string().describe('Description of the argument'), }, execute: async (args, context) => { // Tool implementation return 'result'; }, }, }, }) ``` ### Fields [#fields] * **description**: What the tool does. AI agents use this to decide when to call the tool. * **args**: Zod schema defining the arguments the tool accepts. * **execute**: The function that runs when the tool is called. ## ToolContext [#toolcontext] The `execute` function receives a `ToolContext` with these guaranteed fields: ```ts execute: async (args, context) => { context.logger.info('Tool called', { sessionId: context.sessionId }); if (context.abort.aborted) { return 'Tool cancelled before work started.'; } return `Tool ran for session ${context.sessionId}`; } ``` * `sessionId`: current session ID * `logger`: scoped plugin logger * `abort`: `AbortSignal` for cancellation The tool context does **not** include `client`. If a tool needs to call WhatsApp methods, capture `client` from the plugin `init()` input and use it inside the returned `tool` handlers. ## Example: Lookup Contact Tool [#example-lookup-contact-tool] ```ts export default createPlugin({ meta: { name: 'contact-tools' }, init: async ({ client }) => ({ tool: { lookupContact: { description: 'Look up a contact by phone number', args: { phone: z.string().describe('Phone number to look up'), }, execute: async ({ phone }, context) => { if (typeof phone !== 'string') { return JSON.stringify({ success: false, error: 'phone must be a string' }); } const chatId = `${phone}@c.us`; const contact = await client.getContact(chatId); context.logger.info('Contact looked up', { phone }); return JSON.stringify({ success: true, contact }); }, }, }, }), }); ``` ## Example: Send Message Tool [#example-send-message-tool] ```ts init: async ({ client }) => ({ tool: { sendMessage: { description: 'Send a WhatsApp message to a contact', args: { phone: z.string().describe('Phone number to send to'), message: z.string().describe('Message text'), }, execute: async ({ phone, message }, context) => { if (typeof phone !== 'string' || typeof message !== 'string') { return JSON.stringify({ success: false, error: 'phone and message must be strings' }); } const chatId = `${phone}@c.us`; const messageId = await client.sendText(chatId, message); context.logger.info('Message sent', { phone, messageId }); return JSON.stringify({ success: true, messageId }); }, }, }, }) ``` ## Write a useful tool contract [#write-a-useful-tool-contract] ### Descriptive Names [#descriptive-names] Use a name that says what the action does, such as `lookupContact` or `sendMessage`. ### Clear Descriptions [#clear-descriptions] Describe when to use the tool and what it returns: ```ts description: 'Look up a contact by phone number. Returns the contact name and profile info.' ``` ### Argument Descriptions [#argument-descriptions] Describe each argument, including its expected format: ```ts args: { phone: z.string().describe('Phone number with country code, e.g. 1234567890'), } ``` ### Error Handling [#error-handling] Return useful errors to the caller and log the failure: ```ts execute: async (args, context) => { try { if (typeof args.chatId !== 'string' || typeof args.message !== 'string') { return JSON.stringify({ success: false, error: 'chatId and message must be strings' }); } const messageId = await client.sendText(args.chatId, args.message); return JSON.stringify({ success: true, messageId }); } catch (error) { const message = error instanceof Error ? error.message : String(error); context.logger.error('Failed to send message', { error: message }); return JSON.stringify({ success: false, error: message }); } } ``` ### Return Structured Data [#return-structured-data] Tool handlers return strings. If you want to return structured data to an agent, serialize a JSON object: ```ts return JSON.stringify({ success: true, data: { name: 'John', phone: '1234567890' }, }); ``` ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)** - Build your first plugin * **[MCP Integration](/docs/guides/mcp)** - AI agent integration * **[Hooks reference](/docs/plugins/hooks-reference)** - Available hooks # Dashboard Pages (/docs/plugins/dashboard-pages) # Dashboard Pages [#dashboard-pages] Plugins can supply native components inside Neo's shell and extend its existing Chat view. Neo supplies React, shared UI controls, privacy, calling, and requests scoped to your plugin. A browser module runs with the dashboard's trust; install modules only from plugins you trust. ## Add a page [#add-a-page] Return a root page declaration and serve a self-contained ES module from the plugin's Hono routes. Include the module file in the published package. ```js // In the object returned by init(): routeAuth: 'host', pages: [{ path: '/', title: 'Greeting bot', icon: '👋', module: 'dashboard.js', }], routes: () => { const app = new Hono(); app.get('/dashboard.js', async c => c.body( await readFile(new URL('./dashboard.js', import.meta.url), 'utf8'), 200, { 'Content-Type': 'text/javascript' }, )); app.get('/health', c => c.json({ status: 'Ready' })); return app; }, ``` Import `Hono` from `hono` and `readFile` from `node:fs/promises` in your plugin's server entry. The file URL above assumes the browser module sits beside that entry; preserve the corresponding layout in your package. ```js // dashboard.js — browser module, without bare package imports export function createPage({ React, ui, json }) { const { createElement: h, useEffect, useState } = React; return function GreetingPage() { const [status, setStatus] = useState('Loading…'); useEffect(() => { const controller = new AbortController(); json('health', { signal: controller.signal }) .then(value => setStatus(value.status)) .catch(() => { if (!controller.signal.aborted) setStatus('Unavailable'); }); return () => controller.abort(); }, []); return h('div', { className: 'dashboard-page' }, h(ui.PageHeader, { title: 'Greeting bot' }), h('p', { role: 'status' }, status)); }; } ``` Open **Plugins → Greeting bot**, at `/dashboard/plugins/greeting-bot`. A page without `module` displays the plugin's status metadata. Use `path: '/'` for a custom page; nested plugin page routes aren't implemented. `title`, `icon`, `description`, and optional `order` describe a page. Use an emoji for the sidebar icon. The current sidebar doesn't apply `order` sorting. ## Extend the normal Chat feed [#extend-the-normal-chat-feed] Return `chat: { module: 'chat.js' }` from `init()` and serve that module from your plugin routes. Its `createChat(host)` factory returns a React component receiving `chatId` and `children`. Call `children()` with a toolbar and feed entries: ```js export function createChat({ React }) { const h = React.createElement; return function ChatControls({ chatId, children }) { return children({ toolbar: h('div', { className: 'border-b p-3' }, 'Greeting bot is available in this conversation.'), entries: [], }); }; } ``` A feed entry has `id`, `at` (Unix milliseconds), and React `content`. Add `messageId` to place it beneath a matching loaded WhatsApp message. Otherwise, Neo inserts it into the timeline in time order. Namespace entry IDs within your plugin and keep them stable across updates. New transcripts update the existing entry rather than duplicating it. Return an empty contribution for chats your plugin doesn't handle. Dispose subscriptions when `chatId` changes or the component unmounts. Voxtral uses this interface for voice-note transcripts, call turns, and pause and takeover controls. Its settings and usage stay on its plugin page. ## Host helpers [#host-helpers] * `React` is Neo's own React instance. Don't bundle another copy. * `ui` includes `Button`, `Input`, `Textarea`, `PageHeader`, `DetailSheet`, and BoardUI table components. * `json(path, init)` and `request(path, init)` use the current Easy API connection and its API key header. Paths stay under `/plugins//`. * `watch(path, receive, signal)` consumes `event: snapshot` SSE events. Handle reconnects in your component and fetch a fresh snapshot; it doesn't replay past events. * `usePrivacy()` and `useCalling()` expose Neo's shared hooks. Apply privacy redaction to names and content; use the shared call controller for human audio. * `chatUrl(peer)` returns a link to the person's existing Chat view. Browser modules are fetched with authenticated requests and imported on demand. Keep them self-contained: inline styles or bundle their dependencies ahead of publication. Relative module imports aren't resolved against the plugin route. `routeAuth: 'host'` protects plugin routes with `X-API-Key` when Easy API has a key. Without a key, these routes accept loopback access and reject browser requests from another origin. Remote use needs a configured Easy API key. ## Related [#related] * [Plugin getting started](/docs/plugins/getting-started) * [PluginInput](/docs/plugins/plugin-input) * [HTTP routes](/docs/plugins/hono-routes) * [Voxtral](/docs/guides/voxtral) # Example: voice note transcription (/docs/plugins/example-dictation) # Example: voice note transcription [#example-voice-note-transcription] This example makes a plugin that listens for WhatsApp voice notes. It sends decrypted audio to the OpenAI transcription API. Then, it sends the transcription to the sender. ## Detect voice notes [#detect-voice-notes] Voice notes arrive as messages with specific properties: ```ts type VoiceNoteMessage = { type: 'audio'; mimetype: string; from: string; id: string; }; function getVoiceNote(message: unknown): VoiceNoteMessage | null { if (!message || typeof message !== 'object') return null; const candidate = message as { type?: unknown; mimetype?: unknown; from?: unknown; id?: unknown; }; if (candidate.type !== 'audio') return null; if (typeof candidate.mimetype !== 'string' || !candidate.mimetype.includes('ogg')) return null; if (typeof candidate.from !== 'string' || typeof candidate.id !== 'string') return null; return { type: 'audio', mimetype: candidate.mimetype, from: candidate.from, id: candidate.id, }; } 'message.received': async ({ message, logger }) => { const voiceNote = getVoiceNote(message); if (!voiceNote) return; logger.info('Voice note received'); } ``` ## Decrypt media [#decrypt-media] `client.decryptMedia` returns the decrypted media as a data URL. Decode its base64 content before uploading the audio: ```ts function errorMessage(error: unknown) { return error instanceof Error ? error.message : String(error); } 'message.received': async ({ message, client, logger }) => { const voiceNote = getVoiceNote(message); if (!voiceNote) return; try { const mediaData = await client.decryptMedia(message); // mediaData is a data:audio/ogg;base64,... URL. logger.info('Media decrypted', { hasMedia: mediaData.startsWith('data:') }); } catch (error) { logger.error('Failed to decrypt media', { error: errorMessage(error) }); } } ``` ## Call the transcription API [#call-the-transcription-api] Send the audio to a speech-to-text service. This example uses OpenAI's audio transcription endpoint: ```ts async function transcribe(audioDataUrl: string, apiKey: string): Promise { const match = /^data:([^;,]+);base64,(.+)$/.exec(audioDataUrl); if (!match) throw new Error('Decrypted media was not a base64 data URL'); const [, mimeType, encodedAudio] = match; const audioBuffer = Buffer.from(encodedAudio, 'base64'); const formData = new FormData(); formData.append('file', new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), 'audio.ogg'); formData.append('model', 'whisper-1'); const response = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, }, body: formData, }); if (!response.ok) { throw new Error(`STT API error: ${response.status}`); } const result = await response.json() as { text?: unknown }; if (typeof result.text !== 'string') { throw new Error('STT API response did not include text'); } return result.text; } ``` ## Complete plugin [#complete-plugin] ```ts // src/voice-transcriber.ts import { createPlugin, z } from '@open-wa/plugin-sdk'; type VoiceNoteMessage = { type: 'audio'; mimetype: string; from: string; id: string; }; function getVoiceNote(message: unknown): VoiceNoteMessage | null { if (!message || typeof message !== 'object') return null; const candidate = message as { type?: unknown; mimetype?: unknown; from?: unknown; id?: unknown; }; if (candidate.type !== 'audio') return null; if (typeof candidate.mimetype !== 'string' || !candidate.mimetype.includes('ogg')) return null; if (typeof candidate.from !== 'string' || typeof candidate.id !== 'string') return null; return { type: 'audio', mimetype: candidate.mimetype, from: candidate.from, id: candidate.id, }; } function errorMessage(error: unknown) { return error instanceof Error ? error.message : String(error); } const configSchema = z.object({ apiKey: z.string().min(1, 'STT API key is required'), language: z.string().default('en'), enabled: z.boolean().default(true), }); async function transcribe(audioDataUrl: string, apiKey: string, language: string): Promise { const match = /^data:([^;,]+);base64,(.+)$/.exec(audioDataUrl); if (!match) throw new Error('Decrypted media was not a base64 data URL'); const [, mimeType, encodedAudio] = match; const audioBuffer = Buffer.from(encodedAudio, 'base64'); const formData = new FormData(); formData.append('file', new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), 'audio.ogg'); formData.append('model', 'whisper-1'); formData.append('language', language); const response = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, }, body: formData, }); if (!response.ok) { throw new Error(`STT API error: ${response.status} ${response.statusText}`); } const result = await response.json() as { text?: unknown }; if (typeof result.text !== 'string') { throw new Error('STT API response did not include text'); } return result.text; } export default createPlugin({ meta: { name: 'voice-transcriber' }, configSchema, init: async ({ events, logger, config, client }) => { if (!config.enabled) { logger.info('Voice transcriber disabled'); return; } logger.info('Voice transcriber loaded'); events.on('message.received', async ({ message }) => { const voiceNote = getVoiceNote(message); if (!voiceNote) return; try { const mediaData = await client.decryptMedia(message); logger.info('Media decrypted', { hasMedia: mediaData.startsWith('data:') }); const transcription = await transcribe(mediaData, config.apiKey, config.language); logger.info('Transcription complete', { length: transcription.length }); await client.reply(voiceNote.from, transcription, voiceNote.id); } catch (error) { logger.error('Transcription failed', { error: errorMessage(error) }); } }); }, }); ``` ## Compile and load it [#compile-and-load-it] Using the TypeScript setup in [Publishing a plugin](/docs/plugins/publishing), compile `src/voice-transcriber.ts` to `dist/voice-transcriber.js`. Save this configuration as `wa.config.mjs` beside `src` and `dist`: ```js export default { plugins: [ new URL('./dist/voice-transcriber.js', import.meta.url).href, ], pluginConfig: { 'voice-transcriber': { apiKey: process.env.STT_API_KEY, language: 'en', enabled: true, }, }, }; ``` Start a named session with this config: ```bash npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id dictation-test --host 127.0.0.1 --port 8080 ``` Send an OGG voice note to the connected account. The plugin replies with the transcription; if it does not, check the Easy API logs and confirm that `STT_API_KEY` is set. ## What can go wrong [#what-can-go-wrong] The example logs per-message failures and continues handling later events: * **Decryption failure**: Logged, message skipped * **API failure**: Logged, message skipped * **Empty transcription**: The API's empty text is still sent as a reply. ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)**, build your first plugin * **[PluginClient reference](/docs/plugins/plugin-client)**, available client methods * **[External API patterns](/docs/plugins/external-api-patterns)**, calling external services # Example: OpenAI moderation (/docs/plugins/example-moderation) # Example: OpenAI moderation [#example-openai-moderation] This plugin checks incoming group-message text with the OpenAI Moderation API. Depending on its configuration, it can delete a flagged message when the connected account has permission, reply to the sender, and log the result. The `message.send.before` hook is declared by the SDK but is not active in the host, so apply any outgoing policy before calling the send method. ## Scan incoming messages [#scan-incoming-messages] ```ts function getModeratableMessage(message: unknown) { if (!message || typeof message !== 'object') return null; const candidate = message as { body?: unknown; from?: unknown; chatId?: unknown; id?: unknown; isGroupMsg?: unknown; sender?: { id?: string }; }; if (candidate.isGroupMsg !== true) return null; if (typeof candidate.body !== 'string' || typeof candidate.from !== 'string') return null; if (typeof candidate.chatId !== 'string' || typeof candidate.id !== 'string') return null; return { body: candidate.body, from: candidate.from, chatId: candidate.chatId, id: candidate.id, sender: candidate.sender, }; } events.on('message.received', async ({ message }) => { const msg = getModeratableMessage(message); if (!msg) return; const result = await checkModeration(msg.body, config.apiKey); if (result.flagged) { // Take action } }); ``` ## Call OpenAI moderation [#call-openai-moderation] ```ts type ModerationResult = { flagged: boolean; categories: Record; }; async function checkModeration(text: string, apiKey: string) { const response = await fetch('https://api.openai.com/v1/moderations', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ input: text }), }); if (!response.ok) { throw new Error(`Moderation API error: ${response.status}`); } const data = await response.json() as { results?: ModerationResult[] }; const [result] = data.results ?? []; if (!result) { throw new Error('Moderation API response did not include a result'); } return result; } ``` ## Check messages before sending [#check-messages-before-sending] The host does not call the declared `message.send.before` hook, so it cannot block arbitrary sends. Apply moderation before calling `client.sendText()` in your own workflow, or put the policy in the service that owns outgoing sends: ```ts async function sendModerated(client, chatId, content, logger, apiKey) { const result = await checkModeration(content, apiKey); if (result.flagged) { logger.warn('Outgoing message blocked by moderation', { chatId }); return { sent: false, reason: 'flagged' }; } const messageId = await client.sendText(chatId, content); return { sent: true, messageId }; } ``` ## Act on flagged incoming messages [#act-on-flagged-incoming-messages] The hook runs after a message arrives. You can delete it when the connected account has permission, reply to the sender, or record the result: ```ts if (result.flagged) { // Delete the message if the connected account is a group admin await client.deleteMessage(msg.chatId, msg.id); // Warn the sender await client.reply( msg.from, 'Your message was flagged for inappropriate content.', msg.id ); // Log the action logger.warn('Message flagged and deleted', { from: msg.from, categories: result.categories, }); } ``` ## Group-only moderation [#group-only-moderation] Detect group messages and skip direct messages: ```ts events.on('message.received', async ({ message }) => { const msg = getModeratableMessage(message); if (!msg) return; // Check if this group is in the moderation list if (config.groups.length > 0 && !config.groups.includes(msg.from)) { return; } // Moderate the message }); ``` ## Exempt admins [#exempt-admins] ```ts const exemptAdmins = new Set(config.adminIds || []); events.on('message.received', async ({ message }) => { const msg = getModeratableMessage(message); if (!msg) return; if (exemptAdmins.has(msg.sender?.id)) { return; // Admin messages are exempt } }); ``` ## Delete flagged messages [#delete-flagged-messages] ```ts // Delete the message if (msg.chatId && msg.id) { await client.deleteMessage(msg.chatId, msg.id); } ``` Notes: * The bot must be a group admin to delete messages * There can be a time limit on how old a message can be deleted * Incoming messages can have different deletion behavior than sent messages ## Log actions [#log-actions] ```ts logger.warn('Moderation action taken', { from: msg.from, groupId: msg.chatId, flagged: result.flagged, categories: result.categories, action: 'deleted', }); ``` ## Configuration schema [#configuration-schema] ```ts const configSchema = z.object({ apiKey: z.string().min(1, 'OpenAI API key is required'), groups: z.array(z.string()).default([]), // Empty = all groups adminIds: z.array(z.string()).default([]), // Exempt admins actions: z.object({ delete: z.boolean().default(true), warn: z.boolean().default(true), log: z.boolean().default(true), }).default({}), }); ``` ## Full code example [#full-code-example] ```ts // moderation.ts import { createPlugin, z } from '@open-wa/plugin-sdk'; type ModerationResult = { flagged: boolean; categories: Record; }; type ModeratableMessage = { body: string; from: string; chatId: string; id: string; sender?: { id?: string }; }; function getModeratableMessage(message: unknown): ModeratableMessage | null { if (!message || typeof message !== 'object') return null; const candidate = message as { body?: unknown; from?: unknown; chatId?: unknown; isGroupMsg?: unknown; id?: unknown; sender?: { id?: string }; }; if (candidate.isGroupMsg !== true) return null; if (typeof candidate.body !== 'string' || candidate.body.length === 0) return null; if (typeof candidate.from !== 'string') return null; if (typeof candidate.chatId !== 'string' || typeof candidate.id !== 'string') return null; return { body: candidate.body, from: candidate.from, chatId: candidate.chatId, id: candidate.id, sender: candidate.sender, }; } function errorMessage(error: unknown) { return error instanceof Error ? error.message : String(error); } const configSchema = z.object({ apiKey: z.string().min(1, 'OpenAI API key is required'), groups: z.array(z.string()).default([]), adminIds: z.array(z.string()).default([]), actions: z.object({ delete: z.boolean().default(true), warn: z.boolean().default(true), log: z.boolean().default(true), }).default({}), }); async function checkModeration(text: string, apiKey: string): Promise { const response = await fetch('https://api.openai.com/v1/moderations', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ input: text }), }); if (!response.ok) { throw new Error(`Moderation API error: ${response.status}`); } const data = await response.json() as { results?: ModerationResult[] }; const [result] = data.results ?? []; if (!result) { throw new Error('Moderation API response did not include a result'); } return result; } export default createPlugin({ meta: { name: 'openai-moderation' }, configSchema, init: async ({ events, logger, config, client }) => { logger.info('OpenAI Moderation loaded', { groups: config.groups.length > 0 ? config.groups : 'all', }); const exemptAdmins = new Set(config.adminIds); events.on('message.received', async ({ message }) => { const msg = getModeratableMessage(message); if (!msg) return; // Skip exempt admins if (exemptAdmins.has(msg.sender?.id)) return; // Check group filter if (config.groups.length > 0 && !config.groups.includes(msg.from)) return; try { const result = await checkModeration(msg.body, config.apiKey); if (result.flagged) { if (config.actions.log) { logger.warn('Message flagged', { from: msg.from, categories: result.categories, }); } if (config.actions.delete) { try { await client.deleteMessage(msg.chatId, msg.id); logger.info('Message deleted'); } catch (error) { logger.error('Failed to delete message', { error: errorMessage(error) }); } } if (config.actions.warn) { await client.reply( msg.from, 'Your message was flagged for inappropriate content.', msg.id ); } } } catch (error) { logger.error('Moderation check failed', { error: errorMessage(error) }); // Continue processing. Do not treat API errors as moderation decisions. } }); }, }); ``` ## Load the plugin [#load-the-plugin] Using the TypeScript setup in [Publishing a plugin](/docs/plugins/publishing), compile `src/moderation.ts` to `dist/moderation.js`. Save this configuration as `wa.config.mjs` beside `src` and `dist`: ```js // wa.config.mjs export default { plugins: [ new URL('./dist/moderation.js', import.meta.url).href, ], pluginConfig: { 'openai-moderation': { apiKey: process.env.OPENAI_API_KEY, groups: ['123456789@g.us'], // Specific group IDs adminIds: ['1234567890@c.us'], // Exempt admin IDs actions: { delete: true, warn: true, log: true, }, }, }, }; ``` From the directory containing `wa.config.mjs`, start the session with: ```bash npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id moderation-example --host 127.0.0.1 --port 8080 ``` ## Bound concurrent moderation requests [#bound-concurrent-moderation-requests] ### Concurrent calls [#concurrent-calls] The moderation API is called for every message. If your group has high volume: ```ts import PQueue from 'p-queue'; const queue = new PQueue({ concurrency: 5 }); events.on('message.received', async ({ message }) => { const msg = getModeratableMessage(message); if (!msg) return; await queue.add(async () => { const result = await checkModeration(msg.body, config.apiKey); // ... handle result ... }).catch(error => { logger.error('Moderation request failed', { error: error.message }); }); }); ``` ### Caching [#caching] Cache recent moderation results for identical messages: ```ts const cache = new Map(); const CACHE_TTL = 60 * 1000; // 1 minute function getCached(text: string) { const entry = cache.get(text); if (entry && Date.now() - entry.timestamp < CACHE_TTL) { return entry; } return null; } ``` ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)**, build your first plugin * **[Plugin security model](/docs/plugins/security-model)**, security boundaries * **[External API patterns](/docs/plugins/external-api-patterns)**, calling external services # External API Patterns (/docs/plugins/external-api-patterns) # External API Patterns [#external-api-patterns] Use this guide when a plugin calls an HTTP service, such as an LLM or CRM. It covers response handling, timeouts, retries, and bounded concurrency. ## Making HTTP Requests [#making-http-requests] ### Use fetch [#use-fetch] Node.js 22+ includes native `fetch`: ```ts events.on('message.received', async ({ message }) => { const response = await fetch('https://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}`, }, body: JSON.stringify({ query: message.body }), }); const result = await response.json(); }); ``` ### Adding Peer Dependencies [#adding-peer-dependencies] If you need axios or other HTTP libraries, add them as peer dependencies: ```json { "peerDependencies": { "axios": ">=1.0.0" } } ``` Users install them separately: ```bash npm install axios ``` ## Error Handling [#error-handling] ### HTTP Status Codes [#http-status-codes] ```ts try { const response = await fetch(url); if (response.status === 429) { // Rate limited const retryAfter = response.headers.get('Retry-After'); logger.warn('Rate limited', { retryAfter }); return; // Skip this message. Add a retry queue if it must be retried. } if (response.status === 401) { logger.error('Invalid API key'); return; } if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); } catch (error) { logger.error('API call failed', { error: error.message }); } ``` ### Timeout a request [#timeout-a-request] ```ts try { const response = await fetch(url, { signal: AbortSignal.timeout(10000) }); // Process response } catch (error) { if (error.name === 'TimeoutError' || error.name === 'AbortError') { logger.warn('Request timed out'); } else { logger.error('Request failed', { error: error.message }); } } ``` ### Retry with Exponential Backoff [#retry-with-exponential-backoff] ```ts async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(url, options); if (response.ok) return response; if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000; if (attempt === maxRetries) return response; logger.warn('Rate limited, retrying', { attempt, delay }); await new Promise(r => setTimeout(r, delay)); continue; } return response; // Non-429 error, return it } catch (error) { if (attempt === maxRetries) throw error; const delay = Math.pow(2, attempt) * 1000; logger.warn('Retrying', { attempt, delay }); await new Promise(r => setTimeout(r, delay)); } } } ``` ## Choose how the handler waits [#choose-how-the-handler-waits] ### Handle work asynchronously [#handle-work-asynchronously] An awaited network request keeps this handler pending. If it should return before the request completes, start the work separately and handle its rejection: ```ts // Waits for the request before this hook completes. events.on('message.received', async ({ message }) => { const result = await slowAPI(message.body); await client.sendText(message.from, result); }); // Returns without waiting for the request; rejection is logged. events.on('message.received', async ({ message }) => { processMessage(message).catch(error => { logger.error('Processing failed', { error: error.message }); }); }); ``` ### Queue-Based Processing [#queue-based-processing] Use a queue to control concurrency: ```ts import PQueue from 'p-queue'; const queue = new PQueue({ concurrency: 3 }); events.on('message.received', async ({ message }) => { await queue.add(async () => { const result = await callAPI(message.body); await client.sendText(message.from, result); }).catch(error => { logger.error('Queued request failed', { error: error.message }); }); }); ``` ### Bound how long you wait [#bound-how-long-you-wait] Pass a deadline to the external client when it supports cancellation. `AbortSignal.timeout()` stops waiting after five seconds; the service must honor the signal to cancel the request: ```ts events.on('message.received', async ({ message }) => { try { const result = await callAPI(message.body, { signal: AbortSignal.timeout(5000) }); await client.sendText(message.from, result); } catch (error) { logger.warn('API call skipped', { reason: error.message }); } }); ``` ## Performance [#performance] ### Concurrent Call Limits [#concurrent-call-limits] Set reasonable concurrency limits: ```ts const queue = new PQueue({ concurrency: 5, // Maximum 5 concurrent API calls }); ``` ### Caching [#caching] Cache API responses for identical requests: ```ts const cache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function getCached(key: string) { const entry = cache.get(key); if (entry && Date.now() - entry.timestamp < CACHE_TTL) { return entry.data; } return null; } async function setCached(key: string, data: unknown) { cache.set(key, { data, timestamp: Date.now() }); } ``` ### Memory Management [#memory-management] Clear caches periodically: ```ts // Remove expired entries once a minute. setInterval(() => { const now = Date.now(); for (const [key, entry] of cache.entries()) { if (now - entry.timestamp > CACHE_TTL) { cache.delete(key); } } }, 60 * 1000); ``` ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)** - Build your first plugin * **[Plugin security model](/docs/plugins/security-model)** - Security boundaries * **[Hooks reference](/docs/plugins/hooks-reference)** - Available hooks # Plugin getting started (/docs/plugins/getting-started) # Plugin getting started [#plugin-getting-started] Use a plugin when you want reusable logic to run inside the Easy API process. A plugin can listen for events, call client methods, mount HTTP routes, add dashboard metadata, or register AI tools. Plugins are trusted, dynamically imported modules in the host process. The SDK filters the event emitter and exposes a client proxy, but it does not sandbox Node.js code. Review a plugin and its dependencies before loading it; see the [security model](/docs/plugins/security-model) for the boundary that actually exists. ## Install the SDK [#install-the-sdk] ```bash pnpm add @open-wa/plugin-sdk@5.1.0 hono # or npm install @open-wa/plugin-sdk@5.1.0 hono ``` The SDK provides `createPlugin()`, `defineConfig()`, `z`, and the plugin contract types. The starter below imports `Hono` separately because the route is a regular Hono app. ## Build the starter plugin [#build-the-starter-plugin] Create `plugins/greeting-bot.mjs` with this JavaScript module: ```js import { Hono } from 'hono'; import { createPlugin, defineConfig } from '@open-wa/plugin-sdk'; const configSchema = defineConfig((zod) => zod.object({ greeting: zod.string().default('👋 Welcome!'), triggerWord: zod.string().default('Hi'), })); function getTextMessage(message) { if (!message || typeof message !== 'object') return null; const body = message.body; const from = message.from; if (typeof body !== 'string' || typeof from !== 'string') return null; return { body, from }; } export default createPlugin({ meta: { name: 'greeting-bot', version: '1.0.0', description: 'Replies to a configured greeting word', }, configSchema, init: async ({ client, logger, config }) => ({ 'message.received': async ({ message }) => { const msg = getTextMessage(message); if (!msg || msg.body !== config.triggerWord) return; await client.sendText(msg.from, config.greeting); logger.info('Sent greeting', { from: msg.from }); }, routes: () => { const app = new Hono(); app.get('/health', (c) => c.json({ ok: true, plugin: 'greeting-bot' })); return app; }, pages: [{ path: '/', title: 'Greeting bot', icon: '👋', description: 'The plugin is loaded and its health route is available.', }], }), }); ``` The important shape is `init: async (...) => ({ ... })`: event handlers, `routes`, `pages`, tools, and `dispose` are returned from `init`. They are not sibling options on `createPlugin()`. ## Configure the plugin [#configure-the-plugin] Create `wa.config.js` or `wa.config.ts` beside the `plugins` directory. Either config accepts `export default`; a JavaScript config can also use `module.exports`. Local plugin modules are loaded with Node's dynamic `import()`, so the plugin file itself must contain JavaScript that Node can execute. ```js export default { sessionId: 'my-session', plugins: [ new URL('./plugins/greeting-bot.mjs', import.meta.url).href, ], pluginConfig: { 'greeting-bot': { greeting: 'Hello! Welcome to our service.', triggerWord: 'hello', }, }, }; ``` The `pluginConfig` key is `meta.name` (`greeting-bot`), not the package name or file path. The loader accepts npm package names and importable local references, and it uses a module's default export or named `plugin` export. ### Published plugins install on demand [#published-plugins-install-on-demand] For a published plugin, put its npm package name in `plugins`. Easy API uses a package already installed in your project, or downloads it on the first start and reuses `.open-wa/plugins/packages/` on later starts. It only installs listed plugins and doesn't change your project's dependencies or lockfile. Local file references are never installed from npm. Official `@open-wa/integration-*` packages default to the running open-wa version. Other npm plugins use `latest` for the initial download. To choose a version, append `@` to the package name in the reference. The cache is kept per package and requested version; a versioned reference uses that cache rather than an unversioned project installation. ## Run it [#run-it] Run the CLI from the directory containing your config: ```bash npx @open-wa/wa-automate ``` This binds the API to loopback, so it is reachable only from this machine. If you choose another host, protect `/api` with `--api-key "your-secure-key"` and add a guard to plugin routes before exposing them. See [HTTP routes in plugins](/docs/plugins/hono-routes). After the session is connected, send `hello` from another WhatsApp account. The plugin sends `Hello! Welcome to our service.` back to the sender. The plugin's health endpoint is [http://localhost:8080/plugins/greeting-bot/health](http://localhost:8080/plugins/greeting-bot/health). Open the [local dashboard](http://localhost:8080/dashboard/) and select Greeting bot in the Plugins sidebar. It opens the standard plugin status view; the declaration supplies metadata for that view, not a custom dashboard component. ## If the plugin does not load [#if-the-plugin-does-not-load] * Confirm the CLI loaded the intended `wa.config.js` or `wa.config.ts` file. * Confirm the plugin reference points to JavaScript that Node can import, such as `.mjs` or compiled `.js`. * Confirm the module exports `default createPlugin(...)` or a named `plugin` export. * Confirm `meta.name` is present and that `pluginConfig` uses the same name. * Check for `plugin_installing`, `plugin_installed`, `plugin_loaded`, `plugin_registered`, `plugin_config_invalid`, or `plugin_load_error` in the Easy API logs. A first-time npm install needs registry access; fix any npm error and restart Easy API to retry. If a hook never fires, check the exact hook name against the [Hooks reference](/docs/plugins/hooks-reference). The reference marks hooks that plugins can subscribe to. ## Related [#related] * **[Hooks reference](/docs/plugins/hooks-reference)**: Complete hooks API * **[PluginClient reference](/docs/plugins/plugin-client)**: Available client methods * **[PluginInput breakdown](/docs/plugins/plugin-input)**: What `init()` receives * **[Plugin security model](/docs/plugins/security-model)**: Runtime boundary and restrictions * **[Publishing a plugin](/docs/plugins/publishing)**: How to share your plugin # HTTP Routes in Plugins (/docs/plugins/hono-routes) # HTTP Routes in Plugins [#http-routes-in-plugins] Plugins can mount Hono routes alongside the Easy API for webhooks, health checks, or admin endpoints. A route is a hook returned by the plugin's async `init()` function, and the host mounts that Hono sub-app at `/plugins//`. ## Add a route to the starter plugin [#add-a-route-to-the-starter-plugin] For a complete installation with the event hook, route, and dashboard declaration in one `plugins/greeting-bot.mjs` file, start with [Plugin getting started](/docs/plugins/getting-started). This is the same returned `routes` hook in isolation: ```js import { Hono } from 'hono'; import { createPlugin } from '@open-wa/plugin-sdk'; export default createPlugin({ meta: { name: 'greeting-bot', version: '1.0.0' }, init: async () => ({ routes: () => { const app = new Hono(); app.get('/health', (c) => c.json({ status: 'ok' })); return app; }, }), }); ``` This serves `GET /plugins/greeting-bot/health`, as in the starter. Declare `routes` in the object returned by `init()`; keep plugin metadata and `configSchema` in the top-level `createPlugin()` options. ## Route patterns [#route-patterns] Keep each route inside the Hono app returned from `init()`: ```js init: async () => ({ routes: () => { const app = new Hono(); app.get('/status', (c) => c.json({ plugin: 'greeting-bot', version: '1.0.0', enabled: true, })); app.post('/trigger', async (c) => { const body = await c.req.json(); return c.json({ received: true, data: body }); }); app.get('/contact/:phone', (c) => { const phone = c.req.param('phone'); return c.json({ phone, found: true }); }); return app; }, }), ``` The resulting endpoints are `/plugins/greeting-bot/status`, `/plugins/greeting-bot/trigger`, and `/plugins/greeting-bot/contact/:phone`. ## Route authentication [#route-authentication] Plugin routes are mounted under `/plugins/`, while the Easy API key middleware is mounted under `/api`. A configured Easy API key therefore does not automatically protect plugin routes; add an explicit guard or keep the route behind a trusted network boundary. For a shared-secret guard, close over validated plugin configuration from `init()`: ```js import { Hono } from 'hono'; import { createPlugin, defineConfig } from '@open-wa/plugin-sdk'; const configSchema = defineConfig((z) => z.object({ webhookSecret: z.string().min(1), })); export default createPlugin({ meta: { name: 'greeting-bot', version: '1.0.0' }, configSchema, init: async ({ config }) => ({ routes: () => { const app = new Hono(); app.post('/webhook', async (c) => { const suppliedSecret = c.req.header('X-Plugin-Secret'); if (!suppliedSecret || suppliedSecret !== config.webhookSecret) { return c.json({ error: 'Unauthorized' }, 401); } const event = await c.req.json(); // Process the verified webhook event. return c.json({ received: true, event }); }); return app; }, }), }); ``` Add `webhookSecret` to the existing `configSchema`, then save this complete config as `wa.config.mjs` beside the `plugins` directory: ```js export default { sessionId: 'my-session', plugins: [ new URL('./plugins/greeting-bot.mjs', import.meta.url).href, ], pluginConfig: { 'greeting-bot': { greeting: 'Hello! Welcome to our service.', triggerWord: 'hello', webhookSecret: process.env.GREETING_BOT_WEBHOOK_SECRET, }, }, }; ``` Keep the secret in an environment variable rather than committing its value. This example checks a bearer secret for a private route; it does not implement a provider-specific request signature. If an upstream webhook defines an HMAC or timestamped signing protocol, implement that protocol over the raw request body. ## Common uses [#common-uses] ### Health checks [#health-checks] Return a small JSON response from `GET /health` so an external monitor can verify that the plugin registered. ### Webhook receivers [#webhook-receivers] Parse and validate the request body, then return a response after the plugin accepts the event. Use an idempotency key when the sender may retry delivery. ### OAuth callbacks [#oauth-callbacks] An OAuth callback is still an API route, so mount it from the returned `routes` hook. Validate and consume the one-time `state` created by your authorization-start route before exchanging the code. `consumeOAuthState` and `exchangeOAuthCode` below are application-provided helpers: ```js init: async () => ({ routes: () => { const app = new Hono(); app.get('/oauth/callback', async (c) => { const code = c.req.query('code'); const state = c.req.query('state'); if (!code) return c.json({ error: 'Missing authorization code' }, 400); if (!state || !await consumeOAuthState(state)) { return c.json({ error: 'Invalid OAuth state' }, 400); } await exchangeOAuthCode(code); return c.json({ connected: true }); }); return app; }, }), ``` Store state per authorization attempt, bind it to the initiating user, and expire it after use. The provider-specific token exchange and error handling belong in your helper. ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)** - Build and load `greeting-bot.mjs` * **[Dashboard Pages](/docs/plugins/dashboard-pages)** - Add dashboard metadata to a plugin * **[External API patterns](/docs/plugins/external-api-patterns)** - Calling external services # Hooks reference (/docs/plugins/hooks-reference) # Hooks reference [#hooks-reference] Hooks connect your plugin to the open-wa host. Your plugin's `init()` function returns the handlers and features it wants to use. All hooks are optional. Use only the hook your task needs. ## Host hooks before plugin dispatch [#host-hooks-before-plugin-dispatch] Set `hooks` in the host config to restrict plugins before their handlers run: ```js export default { hooks: { chatFilter: { allow: ['447123456789@c.us', '123456789@lid'], }, beforeEvent: async ({ plugin, event, payload, sessionId }) => { // Return false to skip this plugin handler. return true; }, beforeAction: async ({ plugin, method, args, sessionId }) => { // Return false to block this plugin invocation before dispatch. return true; }, }, }; ``` `chatFilter.allow` contains exact chat IDs. Include verified phone-number and LID IDs explicitly; the filter does not guess whether two IDs are the same person. An empty list blocks all chats. Omit `chatFilter` to leave chats unrestricted. The filter covers named plugin callbacks, catch-all callbacks and direct `input.events` subscriptions, including `once`. A blocked event does not consume a once subscription. Lifecycle events still reach plugins. Chat events without a resolvable chat ID are blocked when a chat filter is configured. Plugin `runtime.invoke`, client methods and `client.ask` also pass through the action hook. The host checks schema-registered recipient parameters, and bound call methods retain the call's chat scope. Unknown operations and chatless writes are blocked under a chat restriction. A rejected invocation throws `PLUGIN_HOOK_REJECTED` with outcome `not_sent`; an event hook failure prevents the handler from running. Custom hooks cannot override `chatFilter`. These host hooks apply to plugins. The following sections describe the listener hooks returned by a plugin's `init()` function. ## Lifecycle hooks [#lifecycle-hooks] ### `core.starting` [#corestarting] Fires when the session is starting up. ```ts 'core.starting': async ({ config }) => { // config is the global wa.config.js contents logger.info('Session starting'); } ``` **Payload:** `{ config: unknown }`, the global configuration object. ### `core.started` [#corestarted] Fires after the session has fully started. ```ts 'core.started': async () => { logger.info('Session ready'); // Start one-time setup here } ``` ### `core.stopping` [#corestopping] Fires when the session is shutting down. ```ts 'core.stopping': async ({ reason }) => { logger.info('Session stopping', { reason }); // Flush queues, save state } ``` **Payload:** `{ reason?: string }`, why the session is stopping. ### `client.ready` [#clientready] Fires when the WhatsApp client is connected and authenticated. ```ts 'client.ready': async ({ sessionId }) => { logger.info('WhatsApp connected', { sessionId }); } ``` **Payload:** `{ sessionId: string }`, the current session identifier. ## Auth hooks [#auth-hooks] ### `auth.qr` [#authqr] Fires when a QR code is emitted. ```ts 'auth.qr': async ({ details }) => { const qr = details?.qr; const attempt = details?.attemptInThisCycle; logger.info('QR code emitted', { attempt }); // Send the QR to your own display channel } ``` **Payload:** * `details.qr`, the QR code string * `details.attemptInThisCycle`, the QR emission count for this cycle, when provided The current host delivers this hook from its internal QR event, so the payload also contains the event envelope (`correlationId`, `ts`, and `step`). It does not include a `sessionId` field. ### `auth.authenticated` [#authauthenticated] Fires after successful authentication. ```ts 'auth.authenticated': async ({ details }) => { if (!details?.isAuthenticated) return; logger.info('Authenticated', { method: details.method }); } ``` **Payload:** an event envelope whose `details` object contains `isAuthenticated` and, when known, the authentication `method`. The hook does not receive a `sessionId` field. ## Message hooks [#message-hooks] ### `message.received` [#messagereceived] Fires when a message is received. ```ts function getTextMessage(message: unknown) { if (!message || typeof message !== 'object') return null; const candidate = message as { body?: unknown; from?: unknown; type?: unknown }; if (candidate.type !== 'chat') return null; if (typeof candidate.body !== 'string' || typeof candidate.from !== 'string') { return null; } return { body: candidate.body, from: candidate.from, type: 'chat' }; } 'message.received': async ({ message }) => { const msg = getTextMessage(message); if (!msg) return; logger.info('Text message received', { from: msg.from }); } ``` **Payload:** `{ message: unknown }`. Examine the necessary fields before you use them. To detect message type, check the `type` field: * `'chat'`, text message * `'image'`, image * `'video'`, video * `'audio'`, voice note or audio message * `'document'`, document/file * `'sticker'`, sticker * `'location'`, location share To detect group messages, check `message.isGroupMsg` or whether the `from` field ends with `@g.us`. ### `message.sent` [#messagesent] This hook currently reports the `message.any` event. Inspect the payload if you need to distinguish sent messages from other message events. ```ts 'message.sent': async ({ message }) => { logger.info('Message sent', { message }); } ``` **Payload:** `{ ctx: EventContext; message: unknown }` ### `message.ack` [#messageack] Fires when a message acknowledgment is received. ```ts 'message.ack': async ({ ack }) => { logger.info('Message acknowledged', { ack }); } ``` **Payload:** `{ ctx: EventContext; ack: unknown }`, the event context and runtime acknowledgment value. ## Message interceptors [#message-interceptors] ### `message.send.before` [#messagesendbefore] The hook is declared in the SDK types, but the host does not call it when a message is sent. Returning it from `init()` does not intercept sends. ```ts 'message.send.before': async ( input: { to: string; content: unknown }, output: { content: unknown; metadata?: Record } ) => { // Modify outgoing text before it is sent if (typeof output.content === 'string') { output.content = output.content.replace(/badword/g, '****'); } } ``` The shown shape documents the declared type only. Use an explicit wrapper around your own `client.sendText()` calls when you need outbound filtering until host support is added. ### `message.send.after` [#messagesendafter] The hook is declared in the SDK types, but the host does not call it after a message is sent. Returning it from `init()` does not observe sends. ```ts 'message.send.after': async ( input: { messageId: string; to: string }, output: { metadata?: Record } ) => { output.metadata = { sentAt: Date.now() }; } ``` The shown shape documents the declared type only. Use a wrapper around your own send calls when you need post-send bookkeeping. ## API routes [#api-routes] ### `routes` [#routes] Return a Hono sub-app that will be mounted at `/plugins//`. ```js import { Hono } from 'hono'; init: async () => ({ routes: () => { const app = new Hono(); app.get('/status', (c) => { return c.json({ plugin: 'my-plugin', status: 'ok' }); }); app.post('/webhook', async (c) => { const body = await c.req.json(); // handle incoming webhook return c.json({ ok: true, body }); }); return app; }, }) ``` Routes are automatically mounted and accessible at `/plugins//`. See **[HTTP routes in plugins](/docs/plugins/hono-routes)** for more details. ## Dashboard pages [#dashboard-pages] ### `pages` [#pages] Declare dashboard pages this plugin wants rendered in the dashboard sidebar. ```js init: async () => ({ pages: [{ path: '/', title: 'Plugin Status', icon: '📊', order: 1, description: 'Current plugin status and configuration', }], }) ``` **Fields:** * `path`, declared route segment; use `/` for the current dashboard entry * `title`, display title in sidebar * `icon`, emoji or Lucide icon name * `order`, optional ordering metadata; the current dashboard does not sort by it * `description`, optional metadata; the current dashboard does not display it The dashboard uses these declarations for its manifest and sidebar, then opens the standard plugin status view. The hook does not add a custom plugin screen. See **[Dashboard pages](/docs/plugins/dashboard-pages)** for more details. ## AI tools [#ai-tools] ### `tool` [#tool] Declare a tool for a host integration to connect to an agent. Keep its description narrow, validate arguments, and avoid actions the agent must not run. The current MCP endpoint does not expose plugin-declared tools; see [AI Tools](/docs/plugins/ai-tools) for that limitation. ```ts init: async ({ client }) => ({ tool: { sendWelcomeMessage: { description: 'Send a welcome message to a contact', args: { chatId: z.string().describe('The chat ID to send to'), name: z.string().describe('The contact name'), }, execute: async (args, context) => { if (typeof args.chatId !== 'string' || typeof args.name !== 'string') { return 'chatId and name must be strings'; } await client.sendText( args.chatId, `Welcome, ${args.name}!` ); context.logger.info('Welcome message sent', { sessionId: context.sessionId }); return `Welcome message sent to ${args.chatId}`; }, }, }, }) ``` **Tool definition:** * `description`, what the tool does (shown to AI agents) * `args`, Zod schema for arguments * `execute`, the implementation function **Tool context:** * `sessionId`, current session ID * `logger`, scoped logger * `abort`, AbortSignal for cancellation The tool context does not include `client`. Tools that need WhatsApp methods must close over `client` from `init()`, as shown above. See **[AI tools](/docs/plugins/ai-tools)** for more details. ## Cleanup [#cleanup] ### `dispose` [#dispose] Called when the session is shutting down. Use this to clean up resources. ```ts init: async ({ logger }) => ({ dispose: async () => { // Close database connections // Flush message queues // Wait for in-flight requests logger.info('Plugin disposed'); }, }) ``` ## Catch-all [#catch-all] ### `event` [#event] Receives every public event. Use this when you need to react to events that do not have a specific hook. ```ts init: async ({ logger }) => ({ event: async ({ event, payload }) => { logger.debug('Event received', { event, payload }); }, }) ``` **Input:** `{ event: string; payload: unknown }` Use specific hooks when possible. They are easier to test and prevent catch-all work on every event. ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)**, build your first plugin * **[PluginClient reference](/docs/plugins/plugin-client)**, available client methods * **[PluginInput breakdown](/docs/plugins/plugin-input)**, what your plugin receives # PluginClient Reference (/docs/plugins/plugin-client) # PluginClient Reference [#pluginclient-reference] The `PluginClient` is the WhatsApp-method interface provided in `PluginInput`. It is a **host-managed proxy** backed by the connected runtime page. `PluginInput` does not include a browser or CDP handle, and the proxy does not create an isolation boundary around trusted plugin code. ## What PluginClient Is [#what-pluginclient-is] * A proxy that forwards method calls to `window.WAPI` in the connected runtime * Available through `client` in your plugin's `PluginInput` * Uses the host's runtime transport without exposing that transport to plugin code * Forwards method names through `ask()` without an SDK method allowlist ## What PluginClient Is Not [#what-pluginclient-is-not] * A browser or CDP interface. The client proxy does not expose CDP commands, DOM access, cookies, or page injection. * Filesystem isolation. `PluginClient` has no filesystem methods; trusted in-process plugin code still has the OS permissions of its host process. * A raw WebSocket. It is a managed proxy whose methods are forwarded by the host transport. ## Generic Method Dispatcher [#generic-method-dispatcher] ### `ask(method, args?)` [#askmethod-args] Call a method by name on the connected runtime's `window.WAPI` object. This is the most flexible way to interact with the runtime; `ask()` does not check the requested name against a safe-method list. ```ts // Send a text message const messageId = await client.ask('sendText', ['1234567890@c.us', 'Hello']); // Get host number const number = await client.ask('getHostNumber'); // Get a contact const contact = await client.ask('getContact', ['1234567890@c.us']); ``` **Parameters:** * `method`, Method name (e.g., `'sendText'`, `'sendImage'`, `'getHostNumber'`) * `args`, Arguments to pass. Can be an array `['arg1', 'arg2']` or a record `{ key: 'value' }` **Returns:** `Promise`, The method's return value, typed via the generic parameter. ## Event Listener [#event-listener] ### `listen(listener, callback)` [#listenlistener-callback] The SDK keeps this method for interface compatibility, but the current host does not support direct client listeners. Calling it logs a warning, does not register the callback, and resolves to the placeholder string `'noop'`. Return event hooks from `init()` instead. ```ts const result = await client.listen('onMessage', (data) => { console.log('Message received:', data); }); // Current host result: 'noop'; the callback is not registered. ``` **Parameters:** * `listener`, Listener name (e.g., `'onMessage'`, `'onAnyMessage'`) * `callback`, Callback function receiving the event data **Returns:** `Promise`, currently `'noop'`. There is no client-level unsubscribe flow; use `events` or named hooks from `PluginInput` for subscriptions. ## Convenience Methods [#convenience-methods] These typed methods are provided for common operations. They all delegate to `ask()` under the hood. ### `sendText(to, content)` [#sendtextto-content] Send a text message. ```ts const messageId = await client.sendText('1234567890@c.us', 'Hello!'); ``` **Returns:** `Promise`, The message ID. ### `sendImage(to, url, filename?, options?)` [#sendimageto-url-filename-options] Send an image from a URL. ```ts const messageId = await client.sendImage( '1234567890@c.us', 'https://example.com/image.jpg', 'image.jpg', { caption: 'Check this out!' } ); ``` ### `sendFile(to, base64, filename?, options?)` [#sendfileto-base64-filename-options] Send a file from base64 data. ```ts const messageId = await client.sendFile( '1234567890@c.us', 'data:application/pdf;base64,JVBERi0...', 'document.pdf', { type: 'document', caption: 'Here is the document' } ); ``` ### `sendLocation(to, lat, lng, text?)` [#sendlocationto-lat-lng-text] Send a location. ```ts await client.sendLocation('1234567890@c.us', '51.5074', '-0.1278', 'London'); ``` ### `sendLinkWithAutoPreview(to, url, text)` [#sendlinkwithautopreviewto-url-text] Send a link with an automatic preview. ```ts await client.sendLinkWithAutoPreview( '1234567890@c.us', 'https://example.com', 'Check out this site' ); ``` ### `reply(to, content, quotedMsgId)` [#replyto-content-quotedmsgid] Reply to a specific message. ```ts await client.reply('1234567890@c.us', 'Got it!', originalMessageId); ``` ### `decryptMedia(message)` [#decryptmediamessage] Decrypt media from a message object. ```ts const mediaDataUrl = await client.decryptMedia(message); // mediaDataUrl is a data URL, such as 'data:image/png;base64,...' ``` Use this for images, voice notes, videos, and documents received in messages. ### `getHostNumber()` [#gethostnumber] Get the phone number of the connected WhatsApp account. ```ts const number = await client.getHostNumber(); // Returns something like '447700900000' ``` ### `getContact(contactId)` [#getcontactcontactid] Get information about a contact. ```ts const contact = await client.getContact('1234567890@c.us'); ``` ### `getAllContacts()` [#getallcontacts] Get all contacts. ```ts const contacts = await client.getAllContacts(); ``` ### `getAllChats()` [#getallchats] Get all chats. ```ts const chats = await client.getAllChats(); ``` ### `sendSeen(chatId)` [#sendseenchatid] Mark a chat as seen (blue ticks). ```ts const success = await client.sendSeen('1234567890@c.us'); ``` **Returns:** `Promise` ## Proxy Fallback [#proxy-fallback] Any method name not listed above can be called on the client object. The proxy forwards it to the runtime; availability depends on the connected runtime exposing that method through `window.WAPI`. ```ts // These all work via the proxy: await client.sendText('123@c.us', 'Hello'); await client.getHostNumber(); await client.someOtherMethod('args'); ``` ## Related [#related] * **[Hooks reference](/docs/plugins/hooks-reference)**, All available hooks * **[PluginInput breakdown](/docs/plugins/plugin-input)**, What your plugin receives * **[Plugin getting started](/docs/plugins/getting-started)**, Build your first plugin # PluginInput Breakdown (/docs/plugins/plugin-input) # PluginInput Breakdown [#plugininput-breakdown] The host passes a `PluginInput` object to your plugin's `init()` function. It contains the event interface, logger, plugin configuration, session identifier, lifetime signal, and client proxy. ```ts interface PluginInput { events: PluginEventEmitter; logger: PluginLogger; config: TConfig; sessionId: string; signal: AbortSignal; client: PluginClient; runtime: PluginRuntime; services: PluginServices; } ``` Each field is described below. ## `events`: Filtered Event Emitter [#events-filtered-event-emitter] The `events` field provides `on()`, `once()`, and `off()`. The host gateway checks event metadata and ignores events marked internal or sensitive. It does not create a sandbox around plugin code. ### What you can do [#what-you-can-do] * Subscribe to allowed events with `on()`, `once()`, and `off()` ```ts events.on('message.received', (payload) => { logger.info('Message received'); }); ``` ### What you cannot do [#what-you-cannot-do] * **Emit through this object:** It has no `emit()` method. * **Subscribe to metadata marked internal or sensitive:** The gateway ignores these requests and logs them. This is based on host event metadata, not name prefixes; unknown names pass to the underlying emitter. The loader dynamically imports plugins into the host process. They retain that process's Node.js and operating-system permissions, so review plugin source and dependencies before loading them. See the [plugin security model](/docs/plugins/security-model) for deployment isolation options. ## `logger`: Scoped Logger [#logger-scoped-logger] The `logger` provides structured logging with automatic plugin name prefixing. ### Methods [#methods] ```ts logger.info('Plugin initialized', { version: '1.0.0' }); logger.warn('API key is missing, using defaults'); logger.error('Failed to connect', { error: 'ECONNREFUSED' }); logger.debug('Processing message', { messageId: 'abc123' }); ``` * `info(message, meta?)`: Informational messages * `warn(message, meta?)`: Warnings * `error(message, meta?)`: Errors * `debug(message, meta?)`: Debug output (only visible when debug logging is enabled) ### Auto-Prefixing [#auto-prefixing] All log output is automatically prefixed with your plugin name: ``` [greeting-bot] Plugin initialized { version: '1.0.0' } ``` ### Log Persistence [#log-persistence] Plugin logs flow through the host's logging system. Whether they are persisted depends on the host's logging configuration. By default, logs are written to stdout/stderr. ## `config`: Validated Configuration [#config-validated-configuration] The `config` field contains your plugin's configuration from `wa.config.js` or `wa.config.ts`. When you provide a `configSchema`, the host validates and parses the value before calling `init()`; without a schema, it passes the configured value through (or `{}` when it is absent). ### Access Pattern [#access-pattern] Your plugin's config is accessed via the `pluginConfig` key in the global config file: ```js // wa.config.js or wa.config.ts export default { pluginConfig: { 'my-plugin': { apiUrl: 'https://api.example.com', apiKey: process.env.MY_PLUGIN_API_KEY, }, }, }; ``` ```ts // src/my-plugin.ts init: async ({ config }) => { // config is { apiUrl: string; apiKey: string } // Already validated against your configSchema console.log(config.apiUrl); } ``` ### Type Inference [#type-inference] When you provide a `configSchema`, the `config` type is automatically inferred: ```ts const configSchema = z.object({ apiUrl: z.string().url(), apiKey: z.string().min(1), retries: z.number().int().positive().default(3), }); // config is typed as { apiUrl: string; apiKey: string; retries: number } init: async ({ config }) => { ... } ``` ## `sessionId`: Current Session [#sessionid-current-session] A string identifier for the current session. ```ts init: async ({ sessionId }) => { logger.info('Plugin loaded for session', { sessionId }); } ``` Use this for: * Logging and debugging * Scoping data to a specific session * Differentiating behavior across multiple sessions ## `signal`: Plugin lifetime [#signal-plugin-lifetime] The host aborts this signal when it starts disposing the plugin. Pass it to cancellable work or check `signal.aborted` before starting another operation. ```ts init: async ({ signal, logger }) => { if (signal.aborted) { logger.debug('Plugin is already stopping'); } return {}; } ``` ## `runtime`: Host Client methods [#runtime-host-client-methods] `runtime.invoke()` calls the attached high-level Client facade on the Easy API host. Use it for prepared media, decryption, and calling with streams; these operations need host services that a raw browser method cannot provide. ```ts const audio = await runtime.invoke('decryptMedia', [message]); const call = await runtime.invoke('getActiveCall'); ``` The facade must be attached before invocation, so start runtime work from `client.ready` or a later event rather than `init()`. Calls return hydrated local objects and can accept Web streams; don't JSON-serialize those values. This bridge has the same process trust as the installed plugin. ## `client`: WhatsApp Method Proxy [#client-whatsapp-method-proxy] The `client` is a host-managed proxy for calling methods exposed by `window.WAPI` in the connected runtime. Its `ask()` method forwards method names and arguments; the SDK does not apply a safe-method allowlist. The proxy does not restrict Node.js imports or replace process isolation. ```ts init: async ({ client }) => { await client.sendText('1234567890@c.us', 'Hello!'); const number = await client.getHostNumber(); } ``` See the **[PluginClient reference](/docs/plugins/plugin-client)** for the complete list of available methods. ## Related [#related] * **[Hooks reference](/docs/plugins/hooks-reference):** What your plugin can return * **[PluginClient reference](/docs/plugins/plugin-client):** Available client methods * **[Plugin security model](/docs/plugins/security-model):** What plugins can and cannot do ## `services`: Shared host services [#services-shared-host-services] Cooperating plugins can use `services.getOrCreate(name, factory)` for one instance per session host. The factory receives a host lifetime signal and returns `{ value, dispose }`. Concurrent acquisitions share the same promise; a failed creation is removed so a later acquisition can try again. The host aborts these services when shutting down, disposes plugins, then disposes the shared services. Do not bind a shared service to the first acquiring plugin's signal. Jev and Voxtral use this for their common conversation state and send journal. # Publishing a Plugin (/docs/plugins/publishing) # Publishing a Plugin [#publishing-a-plugin] Package your plugin as an npm module so users can install it and load it from their open-wa configuration. ## How to Share a Plugin [#how-to-share-a-plugin] **There is no public plugin marketplace.** Sharing a plugin means publishing it as an npm package that users can install and load. The workflow is: 1. Build your plugin as an npm package 2. Publish it to npm (or a private registry) 3. Users install it with `npm install` or `pnpm add` 4. Users load it via the `plugins` array in `wa.config.js` ## Package Structure [#package-structure] ### package.json [#packagejson] ```json { "name": "@myorg/my-plugin", "version": "1.0.0", "description": "A plugin that does X", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "files": [ "dist" ], "peerDependencies": { "@open-wa/plugin-sdk": ">=5.0.0 <6.0.0" }, "scripts": { "build": "tsc", "prepublishOnly": "npm run build" } } ``` ### Key Fields [#key-fields] * **`name`**: Use a scoped name such as `@myorg/my-plugin` to reduce naming collisions. * **`main`**: Set the package entry point for tools that read this legacy field. * **`exports`**: Declare the ESM entry point and type declarations; add a `require` entry if you support CommonJS consumers. * **`types`**: Point TypeScript users to the generated declarations. * **`files`**: Publish only the files users need, such as `dist`. * **`peerDependencies`**: Declare the SDK versions your plugin supports. ### TypeScript Compilation [#typescript-compilation] Compile your plugin before publishing: ```json // tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "declaration": true, "outDir": "dist", "rootDir": "src", "strict": true, "esModuleInterop": true }, "include": ["src"] } ``` ## Versioning [#versioning] ### Semantic Versioning [#semantic-versioning] Follow semver (`MAJOR.MINOR.PATCH`): * **MAJOR**: Breaking changes, such as removing a hook or changing a required config field. * **MINOR**: Backward-compatible features, such as adding an optional config field. * **PATCH**: Backward-compatible fixes. ### Compatibility with open-wa Versions [#compatibility-with-open-wa-versions] Note which versions of open-wa your plugin is compatible with: ```json { "peerDependencies": { "@open-wa/plugin-sdk": ">=5.0.0 <6.0.0" }, "engines": { "node": ">=22.21.1" } } ``` ### Communicating Breaking Changes [#communicating-breaking-changes] For a major release, include migration instructions in the README and changelog. If you plan to remove a feature, give users a deprecation period where practical. ## Telling Users How to Load It [#telling-users-how-to-load-it] ### Installation [#installation] ```bash npm install @myorg/my-plugin # or pnpm add @myorg/my-plugin ``` ### Configuration [#configuration] Document the exact `wa.config.mjs` entries users need: ```js // wa.config.mjs export default { plugins: [ '@myorg/my-plugin', ], pluginConfig: { 'my-plugin': { // Document each field here apiUrl: 'https://api.example.com', apiKey: process.env.MY_PLUGIN_API_KEY, enabled: true, }, }, }; ``` Use the package name in `plugins`; the `pluginConfig` key must match the plugin's `meta.name`. ## Documenting Your Plugin [#documenting-your-plugin] ### README Template [#readme-template] The plugin README must include: ````markdown # @myorg/my-plugin Brief description of what the plugin does. ## Installation ```bash npm install @myorg/my-plugin ``` ## Configuration Add to your `wa.config.mjs`: ```js export default { plugins: ['@myorg/my-plugin'], pluginConfig: { 'my-plugin': { // necessary: Description apiKey: process.env.MY_PLUGIN_API_KEY, // optional: Description (default: value) optionName: 'default', }, }, }; ``` ### Configuration Options | Field | Type | Required | Default | Description | |---|---|---|---|---| | apiKey | string | Yes | - | Your API key | | optionName | string | No | "default" | Description | ## Features - Feature 1 - Feature 2 ## Troubleshooting ### Common issue 1 Solution... ### Common issue 2 Solution... ```` ## Check the published package locally [#check-the-published-package-locally] ### Load a local package build [#load-a-local-package-build] Before publishing to npm, test locally: ```js // wa.config.mjs export default { plugins: [ new URL('./path/to/my-plugin/dist/index.js', import.meta.url).href, ], pluginConfig: { 'my-plugin': { // test config }, }, }; ``` ### Use a named session [#use-a-named-session] Use a named session while trying the plugin: ```bash npx @open-wa/wa-automate@5.1.0 --config ./wa.config.mjs --session-id plugin-test --host 127.0.0.1 --port 8080 ``` ### Multi-Session Compatibility [#multi-session-compatibility] If your plugin stores session-specific data, run two named sessions and verify each plugin instance reads and writes data for its own session. ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)**, Build your first plugin * **[Hooks reference](/docs/plugins/hooks-reference)**, Available hooks * **[Plugin security model](/docs/plugins/security-model)**, Plugin permissions and isolation # Anatomy of a Plugin (/docs/plugins/reference-plugin-walkthrough) # Anatomy of a Plugin [#anatomy-of-a-plugin] This page explains the event and lifecycle patterns used by the webhook and Chatwoot integrations in this checkout. Keep the source files as the authority when an integration adds provider-specific behavior. ## The Webhook Plugin [#the-webhook-plugin] The webhook plugin pushes public WhatsApp events to an external HTTP endpoint. It demonstrates catch-all event forwarding, configuration, bounded delivery, retries, and disposal. ### File Location [#file-location] ``` integrations/webhook/src/plugin.ts ``` ### Structure Breakdown [#structure-breakdown] ```ts export default createPlugin({ meta: { name: 'webhook' }, configSchema: webhookConfigSchema, init: async ({ logger, config, sessionId }) => ({ event: async ({ event, payload }) => { // The integration's deliverer applies filtering, queueing and retries. await deliver(event, payload, config, sessionId); }, dispose: async () => { // Drain and close the delivery runtime. }, }), }); ``` ### Key Patterns [#key-patterns] * **`meta.name`**: Used for logging prefix and config key mapping * **`configSchema`**: Zod schema that validates `pluginConfig.webhook` from `wa.config.js` * **`init`**: Creates the delivery runtime and returns the plugin's `Hooks` object * **`event`**: Receives public events; the integration filters and queues the events selected by its config * **`dispose`**: Releases the delivery runtime and drains its queue during shutdown ### Event Forwarding Pattern [#event-forwarding-pattern] ```ts event: async ({ event, payload }) => { await fetch(config.url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event, data: payload, timestamp: Date.now(), }), }); }); ``` This is a shape for a simple receiver. The supplied integration adds event filtering, queue capacity, retry policy, request timeouts, and optional durable delivery around the HTTP call. ## The Chatwoot Plugin [#the-chatwoot-plugin] The Chatwoot plugin bridges WhatsApp into Chatwoot conversations. It demonstrates bidirectional message handling, contact management, and media mapping. ### File Location [#file-location-1] ``` integrations/chatwoot/src/plugin.ts ``` ### Structure Breakdown [#structure-breakdown-1] ```ts export default createPlugin({ meta: { name: 'chatwoot' }, configSchema: chatwootConfigSchema, init: async ({ events, logger, config, client }) => { ... }, }); ``` ### Key Patterns [#key-patterns-1] * **Bidirectional sync**: WhatsApp messages flow into Chatwoot, and Chatwoot agent replies flow back to WhatsApp * **Contact mapping**: Phone numbers are mapped to Chatwoot contacts * **Media handling**: Media messages are uploaded to Chatwoot as attachments ### Contact Sync Pattern [#contact-sync-pattern] ```ts async function getOrCreateContact(phoneNumber: string) { // Check if contact exists in Chatwoot // If not, create a new contact // Return the contact ID } ``` ### Message Routing Pattern [#message-routing-pattern] ```ts events.on('message.received', async ({ message }) => { const contactId = await getOrCreateContact(message.from); await createChatwootMessage(contactId, message.body); }); ``` ## Patterns to Reuse [#patterns-to-reuse] ### 1. Config Validation [#1-config-validation] Always provide a `configSchema`: ```ts const configSchema = z.object({ url: z.string().url(), apiKey: z.string().optional(), enabled: z.boolean().default(true), }); ``` ### 2. Event Subscription [#2-event-subscription] Subscribe to specific events instead of a catch-all subscription: ```ts events.on('message.received', handler); events.on('client.ready', handler); events.on('core.stopping', handler); ``` ### 3. Error Handling [#3-error-handling] Wrap external calls in try-catch and log errors: ```ts try { await fetchExternalAPI(data); } catch (error) { logger.error('External API failed', { error: error.message }); } ``` ### 4. Graceful Degradation [#4-graceful-degradation] Keep the host alive when an external service fails, but make the retry or persistence policy explicit: ```ts events.on('message.received', async ({ message }) => { try { await processMessage(message); } catch (error) { logger.error('Processing failed', { error: error.message }); // Add an explicit retry or durable queue if the event must not be lost. } }); ``` ### 5. Lifecycle Awareness [#5-lifecycle-awareness] Handle session state changes: ```ts events.on('core.stopping', async () => { // Flush queues, save state, close connections logger.info('Plugin shutting down'); }); ``` ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started)** - Build your first plugin * **[Hooks reference](/docs/plugins/hooks-reference)** - Available hooks * **[Plugin security model](/docs/plugins/security-model)** - Security boundaries # Plugin Security Model (/docs/plugins/security-model) # Plugin Security Model [#plugin-security-model] Plugins load as JavaScript modules in the open-wa Node.js process and run with that process's operating-system permissions. The SDK does not create a filesystem, network, or process sandbox, so review a plugin and its dependencies before loading it. ## API restrictions [#api-restrictions] ### Cannot Emit Events [#cannot-emit-events] The `events` object supplied to `init()` exposes `on()`, `once()`, and `off()`; it has no `emit()` method. This limits access through that object, but it does not prevent plugin code from importing Node.js modules or using other host-process capabilities. ```ts // This is NOT possible: events.emit('custom.event', data); // No emit method exists ``` ### Internal and sensitive events [#internal-and-sensitive-events] The gateway checks the event metadata registered by the host. Events marked `internal` or `sensitive` are ignored and logged when a plugin subscribes; the policy is metadata-based, not a blanket block on particular name prefixes. Unknown event names are passed to the underlying emitter, so the gateway should not be treated as a sandbox or as an authorization boundary for arbitrary code. ### No direct browser access through the SDK [#no-direct-browser-access-through-the-sdk] `PluginInput` includes a **client proxy**, not a browser or CDP handle. The client can call WhatsApp methods exposed by `window.WAPI`; `ask(method, args)` forwards the requested method name to that runtime, so it is not a method allowlist. The SDK client interface does not expose methods to: * Execute CDP commands * Manipulate the browser DOM directly * Access browser cookies or storage directly * Inject scripts into the WhatsApp Web page ## What the SDK does not isolate [#what-the-sdk-does-not-isolate] Node's dynamic `import()` runs both a plugin's top-level code and `init()` in the host process. The event gateway and client proxy describe SDK interfaces; they do not limit a plugin's Node.js imports or OS permissions. Review the plugin source, transitive dependencies, and requested permissions before installation. If code must be treated as untrusted, run it in a separately managed service with an operating-system or container boundary and communicate through an external API. Configure that isolation in your deployment environment; open-wa does not provide a plugin sandbox. ## What Plugins Can Do [#what-plugins-can-do] ### Subscribe to Events Allowed by the Gateway [#subscribe-to-events-allowed-by-the-gateway] ```ts events.on('message.received', (payload) => { ... }); events.once('client.ready', () => { ... }); ``` ### Call WhatsApp Methods Through the Client Proxy [#call-whatsapp-methods-through-the-client-proxy] The convenience methods and `ask()` call methods provided by the connected WhatsApp runtime: ```ts await client.sendText('123@c.us', 'Hello'); await client.getHostNumber(); await client.ask('getAllChats'); ``` ### Mount HTTP routes [#mount-http-routes] ```ts init: async () => ({ routes: () => { const app = new Hono(); app.get('/health', (c) => c.json({ ok: true })); return app; }, }) ``` The host mounts the returned Hono app at `/plugins//`. ### Declare AI Tools [#declare-ai-tools] ```ts init: async () => ({ tool: { myTool: { description: 'Does something useful', args: { input: z.string() }, execute: async (args, ctx) => { ... }, }, }, }) ``` This declares a tool in the plugin hooks. The current MCP endpoint does not expose plugin-declared tools; see [AI Tools](/docs/plugins/ai-tools) for the host integration requirement. ### Add dashboard pages [#add-dashboard-pages] ```ts init: async () => ({ pages: [{ path: '/', title: 'Status', icon: '📊' }], }) ``` The dashboard reads these declarations from the plugin manifest and renders its standard plugin status view. They do not grant a plugin a custom React component or a separate process. ## Config Validation [#config-validation] ### How It Works [#how-it-works] If you provide a `configSchema` in your plugin definition, the host validates the configuration **before** calling `init()`. ```ts export default createPlugin({ meta: { name: 'my-plugin' }, configSchema: z.object({ apiUrl: z.string().url(), apiKey: z.string().min(1), }), init: async ({ config }) => { // config is guaranteed to match the schema }, }); ``` ### If Validation Fails [#if-validation-fails] * The plugin will **not** register * An error is logged with details about what failed * During client creation, the registration error aborts plugin setup and the client start path; the current host does not skip the invalid plugin and continue with the remaining list ### Handling Missing Config Gracefully [#handling-missing-config-gracefully] Use Zod defaults and optional fields: ```ts const configSchema = z.object({ apiUrl: z.string().url().default('https://api.example.com'), apiKey: z.string().optional(), retries: z.number().int().positive().default(3), }); ``` If `apiKey` is optional, your plugin must accept an `undefined` value: ```ts init: async ({ config }) => { if (!config.apiKey) { logger.warn('No API key configured, some features will be disabled'); } } ``` ## Secret Management [#secret-management] ### Environment Variables [#environment-variables] Reference environment variables in your `wa.config.js`: ```js // wa.config.js export default { pluginConfig: { 'my-plugin': { apiKey: process.env.MY_PLUGIN_API_KEY, }, }, }; ``` ### Read values from a `.env` file [#read-values-from-a-env-file] If your app loads dotenv values, keep them in a `.env` file in the project root: ``` MY_PLUGIN_API_KEY=sk-abc123... ``` The config file can read from `process.env` which picks up `.env` values if your setup loads them. ### Are Config Values Logged? [#are-config-values-logged] Plugin config values are **not** automatically logged. But if your plugin logs the config object directly, secrets will appear in the logs. Always redact sensitive fields: ```ts // BAD: logs the API key logger.info('Config loaded', { config }); // GOOD: logs only non-sensitive fields logger.info('Config loaded', { apiUrl: config.apiUrl, hasKey: !!config.apiKey }); ``` ### Validating API Keys Are Present [#validating-api-keys-are-present] ```ts const configSchema = z.object({ apiKey: z.string().min(1, 'API key is required'), }); ``` This will fail validation with a clear error message if the key is missing or empty. ### Rotating Keys Without Restart [#rotating-keys-without-restart] Currently, config is validated once at plugin initialization. To replace keys: 1. Update the environment variable or config file 2. Restart the session 3. The plugin will reload with the new key ## Error Handling Patterns [#error-handling-patterns] ### API Failures (Rate Limits, Timeouts, 401) [#api-failures-rate-limits-timeouts-401] ```ts 'message.received': async ({ message }) => { try { const result = await fetchExternalAPI(message); } catch (error) { if (error.status === 429) { logger.warn('Rate limited, will retry', { retryAfter: error.retryAfter }); // Queue for retry } else if (error.status === 401) { logger.error('Invalid API key'); // Notify admin or disable feature } else { logger.error('API call failed', { error: error.message }); // Continue processing other messages } } } ``` ### Network Errors [#network-errors] ```ts try { const response = await fetch(url, { signal: AbortSignal.timeout(10000) }); } catch (error) { if (error.name === 'TimeoutError') { logger.warn('Request timed out'); } else if (error.code === 'ECONNREFUSED') { logger.error('Connection refused'); } } ``` ### Malformed Messages [#malformed-messages] Always validate message data before processing: ```ts 'message.received': async ({ message }) => { const msg = message as { body?: string; from?: string }; if (!msg.body || !msg.from) { logger.debug('Skipping malformed message'); return; } // Process the message } ``` ### WhatsApp Connection Drops [#whatsapp-connection-drops] Use lifecycle hooks to detect connection state: ```ts 'core.stopping': async ({ reason }) => { logger.info('Session stopping', { reason }); // Flush queues, save state }, 'client.ready': async ({ sessionId }) => { logger.info('Reconnected', { sessionId }); // Resume processing }, ``` ### Throw an error or continue [#throw-an-error-or-continue] * **`logger.error()` and continue:** For recoverable errors (network timeouts, rate limits), the plugin keeps processing other messages. * **Throw from an event handler:** The host catches the rejection and logs a `plugin_hook_error`; it does not provide an automatic retry or unregister operation. * **Throw from `init()`:** Registration fails and the client start path aborts, so use this for unrecoverable initialization errors. * **`dispose()`:** Use this hook to release resources during host shutdown. The current API has no plugin-level unregister operation. * **Queue for retry:** For transient failures (network errors, rate limits), implement retry with exponential backoff. ## Related [#related] * **[Plugin getting started](/docs/plugins/getting-started):** Build your first plugin * **[PluginInput breakdown](/docs/plugins/plugin-input):** What your plugin receives * **[External API patterns](/docs/plugins/external-api-patterns):** Calling external services from plugins # Aliases (/docs/reference/client/aliases) # Aliases [#aliases] ## Function aliases [#function-aliases] Alternative names (including namespaced forms) that resolve to a canonical method. | Canonical method | Alias | Deprecated | Kind | | ---------------------------- | ----------------------------------- | ---------- | ---------- | | `acceptCall` | `calls.acceptCall` | No | Namespaced | | `addLabel` | `labels.add` | No | Namespaced | | `addParticipant` | `addGroupParticipant` | No | Flat | | `addParticipant` | `groups.addParticipant` | No | Namespaced | | `approveGroupJoinRequest` | `groups.approveJoinRequest` | No | Namespaced | | `archiveChat` | `chats.archive` | No | Namespaced | | `blockContact` | `contactBlock` | Yes | Flat | | `blockContact` | `contacts.block` | No | Namespaced | | `blockContact` | `contacts.contactBlock` | Yes | Namespaced | | `cancelEvent` | `events.cancelEvent` | No | Namespaced | | `checkNumberStatus` | `contacts.checkNumberStatus` | No | Namespaced | | `checkReadReceipts` | `contacts.checkReadReceipts` | No | Namespaced | | `checkReadReceipts` | `contacts.getReadReceipts` | No | Namespaced | | `checkReadReceipts` | `getReadReceipts` | No | Flat | | `clearCallAudio` | `calls.clearCallAudio` | No | Namespaced | | `clearChat` | `chats.clear` | No | Namespaced | | `createGroup` | `groups.create` | No | Namespaced | | `decryptMedia` | `media.decrypt` | No | Namespaced | | `deleteAllStatus` | `deleteAllStatuses` | No | Flat | | `deleteAllStatus` | `status.deleteAll` | No | Namespaced | | `deleteChat` | `chats.delete` | No | Namespaced | | `deleteMessage` | `messages.delete` | No | Namespaced | | `deleteStatus` | `status.delete` | No | Namespaced | | `demoteParticipant` | `demoteGroupParticipant` | No | Flat | | `demoteParticipant` | `groups.demoteParticipant` | No | Namespaced | | `downloadMedia` | `media.download` | No | Namespaced | | `editEvent` | `events.editEvent` | No | Namespaced | | `editMessage` | `messages.edit` | No | Namespaced | | `endCall` | `calls.endCall` | No | Namespaced | | `forwardMessages` | `messages.forward` | No | Namespaced | | `getActiveCall` | `calls.getActiveCall` | No | Namespaced | | `getAlbumMessages` | `messages.getAlbum` | No | Namespaced | | `getAllChatIds` | `chatIds.all` | No | Namespaced | | `getAllChatIds` | `chatIds.getAll` | No | Namespaced | | `getAllChatIds` | `chatIds.list` | No | Namespaced | | `getAllChatIds` | `chats.ids` | No | Namespaced | | `getAllChatIds` | `listChatIds` | No | Flat | | `getAllChats` | `chats.all` | No | Namespaced | | `getAllChats` | `chats.getAll` | No | Namespaced | | `getAllChats` | `chats.list` | No | Namespaced | | `getAllChats` | `listChats` | No | Flat | | `getAllCommunities` | `communities.all` | No | Namespaced | | `getAllCommunities` | `communities.getAll` | No | Namespaced | | `getAllCommunities` | `communities.list` | No | Namespaced | | `getAllCommunities` | `listCommunities` | No | Flat | | `getAllContacts` | `contacts.all` | No | Namespaced | | `getAllContacts` | `contacts.getAll` | No | Namespaced | | `getAllContacts` | `contacts.list` | No | Namespaced | | `getAllContacts` | `listContacts` | No | Flat | | `getAllGroups` | `groups.all` | No | Namespaced | | `getAllGroups` | `groups.getAll` | No | Namespaced | | `getAllGroups` | `groups.list` | No | Namespaced | | `getAllGroups` | `listGroups` | No | Flat | | `getAllLabels` | `labels.all` | No | Namespaced | | `getAllLabels` | `labels.getAll` | No | Namespaced | | `getAllLabels` | `labels.list` | No | Namespaced | | `getAllLabels` | `listLabels` | No | Flat | | `getAllMessages` | `listMessages` | No | Flat | | `getAllMessages` | `messages.all` | No | Namespaced | | `getAllMessages` | `messages.getAll` | No | Namespaced | | `getAllMessages` | `messages.list` | No | Namespaced | | `getBatteryLevel` | `session.getBatteryLevel` | No | Namespaced | | `getBlockedIds` | `contacts.getBlockedIds` | No | Namespaced | | `getBusinessProducts` | `business.getProducts` | No | Namespaced | | `getBusinessProducts` | `business.getProfilesProducts` | Yes | Namespaced | | `getBusinessProducts` | `getBusinessProfilesProducts` | Yes | Flat | | `getBusinessProfile` | `business.getProfile` | No | Namespaced | | `getCallCapabilities` | `calls.getCallCapabilities` | No | Namespaced | | `getChat` | `chats.get` | No | Namespaced | | `getChat` | `chats.getById` | No | Namespaced | | `getChat` | `getChatById` | No | Flat | | `getChatsByLabel` | `labels.getChats` | No | Namespaced | | `getChatsByLabel` | `labels.getChatsBy` | No | Namespaced | | `getChatWithNonContacts` | `chats.getWithNonContacts` | No | Namespaced | | `getCommonGroups` | `contacts.getCommonGroups` | No | Namespaced | | `getCommunityAdminIds` | `communities.getAdminIds` | No | Namespaced | | `getCommunityAdmins` | `communities.getAdmins` | No | Namespaced | | `getCommunityInfo` | `communities.getInfo` | No | Namespaced | | `getCommunityParticipantIds` | `communities.getParticipantIds` | No | Namespaced | | `getCommunityParticipants` | `communities.getParticipants` | No | Namespaced | | `getConnectionState` | `session.getConnectionState` | No | Namespaced | | `getContact` | `contacts.get` | No | Namespaced | | `getEvent` | `events.getEvent` | No | Namespaced | | `getEventResponses` | `events.getEventResponses` | No | Namespaced | | `getEvents` | `events.get` | No | Namespaced | | `getFeatures` | `session.getFeatures` | No | Namespaced | | `getGroupAdmins` | `groups.getAdmins` | No | Namespaced | | `getGroupInfo` | `groups.getInfo` | No | Namespaced | | `getGroupInviteLink` | `groups.getInviteLink` | No | Namespaced | | `getGroupMembers` | `groups.getMembers` | No | Namespaced | | `getGroupMembersId` | `getGroupMemberIds` | No | Flat | | `getGroupMembersId` | `groups.getMemberIds` | No | Namespaced | | `getGroupMembersId` | `groups.getMembersId` | No | Namespaced | | `getHostNumber` | `session.getHostNumber` | No | Namespaced | | `getKickedGroups` | `groups.getKicked` | No | Namespaced | | `getLicenseFeatures` | `session.getLicenseFeatures` | No | Namespaced | | `getLicenseType` | `session.getLicenseType` | No | Namespaced | | `getLoadedMessageCount` | `getAmountOfLoadedMessages` | Yes | Flat | | `getLoadedMessageCount` | `session.getAmountOfLoadedMessages` | Yes | Namespaced | | `getLoadedMessageCount` | `session.getLoadedMessageCount` | No | Namespaced | | `getMe` | `session.getMe` | No | Namespaced | | `getMessageById` | `getMessage` | No | Flat | | `getMessageById` | `messages.get` | No | Namespaced | | `getMessageById` | `messages.getById` | No | Namespaced | | `getMessageInfo` | `messages.getInfo` | No | Namespaced | | `getMessagesForLLM` | `getGptArray` | Yes | Flat | | `getMessagesForLLM` | `messages.getForLLM` | No | Namespaced | | `getMessagesForLLM` | `messages.getGptArray` | Yes | Namespaced | | `getMyLastMessage` | `messages.getMyLast` | No | Namespaced | | `getNumberProfile` | `contacts.getNumberProfile` | No | Namespaced | | `getOrder` | `business.getOrder` | No | Namespaced | | `getPollResults` | `messages.getPollResults` | No | Namespaced | | `getProcessStats` | `session.getProcessStats` | No | Namespaced | | `getProfilePicture` | `contacts.getProfilePicFromServer` | Yes | Namespaced | | `getProfilePicture` | `contacts.getProfilePicture` | No | Namespaced | | `getProfilePicture` | `getProfilePicFromServer` | Yes | Flat | | `getSnapshot` | `session.getSnapshot` | No | Namespaced | | `getSnapshot` | `session.takeScreenshot` | No | Namespaced | | `getSnapshot` | `takeScreenshot` | No | Flat | | `getStarredMessages` | `messages.getStarred` | No | Namespaced | | `getStatus` | `mystatus.getStatus` | No | Namespaced | | `getStories` | `getStatuses` | No | Flat | | `getStories` | `status.get` | No | Namespaced | | `getStories` | `status.getAll` | No | Namespaced | | `getStories` | `status.getStories` | No | Namespaced | | `getUnsentMessages` | `messages.getUnsent` | No | Namespaced | | `getUserAgent` | `getGeneratedUserAgent` | Yes | Flat | | `getUserAgent` | `session.getGeneratedUserAgent` | Yes | Namespaced | | `getUserAgent` | `session.getUserAgent` | No | Namespaced | | `getVCards` | `messages.getVCards` | No | Namespaced | | `getWAVersion` | `getWhatsAppVersion` | No | Flat | | `getWAVersion` | `session.getWAVersion` | No | Namespaced | | `getWAVersion` | `session.getWhatsAppVersion` | No | Namespaced | | `healthCheck` | `session.healthCheck` | No | Namespaced | | `isChatOnline` | `chats.isOnline` | No | Namespaced | | `isPlugged` | `getIsPlugged` | Yes | Flat | | `isPlugged` | `session.getIsPlugged` | Yes | Namespaced | | `isPlugged` | `session.isPlugged` | No | Namespaced | | `joinGroupViaLink` | `groups.join` | No | Namespaced | | `joinGroupViaLink` | `groups.joinViaLink` | No | Namespaced | | `joinGroupViaLink` | `joinGroup` | No | Flat | | `leaveGroup` | `groups.leave` | No | Namespaced | | `loadEarlierMessages` | `loadOlderMessages` | No | Flat | | `loadEarlierMessages` | `messages.loadEarlier` | No | Namespaced | | `loadEarlierMessages` | `messages.loadOlder` | No | Namespaced | | `markAsRead` | `chats.markAsRead` | No | Namespaced | | `markAsUnread` | `chats.markAsUnread` | No | Namespaced | | `muteCall` | `calls.muteCall` | No | Namespaced | | `muteChat` | `chats.mute` | No | Namespaced | | `observeCallAudio` | `calls.observeCallAudio` | No | Namespaced | | `pinChat` | `chats.pin` | No | Namespaced | | `pinMessage` | `messages.pin` | No | Namespaced | | `postImageStatus` | `status.postImage` | No | Namespaced | | `postTextStatus` | `status.postText` | No | Namespaced | | `postVideoStatus` | `status.postVideo` | No | Namespaced | | `promoteParticipant` | `groups.promoteParticipant` | No | Namespaced | | `promoteParticipant` | `promoteGroupParticipant` | No | Flat | | `react` | `messages.react` | No | Namespaced | | `rejectCall` | `calls.rejectCall` | No | Namespaced | | `rejectGroupJoinRequest` | `groups.rejectJoinRequest` | No | Namespaced | | `removeLabel` | `labels.remove` | No | Namespaced | | `removeParticipant` | `groups.removeParticipant` | No | Namespaced | | `removeParticipant` | `removeGroupParticipant` | No | Flat | | `requestPhoneNumber` | `contacts.requestPhoneNumber` | No | Namespaced | | `respondToEvent` | `events.respondToEvent` | No | Namespaced | | `revokeGroupInviteLink` | `groups.revokeInviteLink` | No | Namespaced | | `sendAdvancedButtons` | `messages.sendAdvancedButtons` | No | Namespaced | | `sendAlbum` | `messages.sendAlbum` | No | Namespaced | | `sendAudio` | `messages.sendAudio` | No | Namespaced | | `sendBanner` | `messages.sendBanner` | No | Namespaced | | `sendButtons` | `messages.sendButtons` | No | Namespaced | | `sendContact` | `messages.sendContact` | No | Namespaced | | `sendEvent` | `events.sendEvent` | No | Namespaced | | `sendFile` | `messages.sendFile` | No | Namespaced | | `sendFileFromUrl` | `messages.sendFileFromUrl` | No | Namespaced | | `sendImage` | `messages.sendImage` | No | Namespaced | | `sendInteractive` | `messages.sendInteractive` | No | Namespaced | | `sendLinkWithAutoPreview` | `messages.sendLink` | No | Namespaced | | `sendLinkWithAutoPreview` | `messages.sendLinkWithAutoPreview` | No | Namespaced | | `sendLinkWithAutoPreview` | `sendLink` | No | Flat | | `sendListMessage` | `messages.sendList` | No | Namespaced | | `sendListMessage` | `sendList` | No | Flat | | `sendLocation` | `messages.sendLocation` | No | Namespaced | | `sendMessageWithThumb` | `messages.sendLinkWithThumbnail` | No | Namespaced | | `sendMessageWithThumb` | `messages.sendWithThumb` | No | Namespaced | | `sendMessageWithThumb` | `sendLinkWithThumbnail` | No | Flat | | `sendMultipleContacts` | `messages.sendContacts` | No | Namespaced | | `sendMultipleContacts` | `messages.sendMultipleContacts` | No | Namespaced | | `sendMultipleContacts` | `sendContacts` | No | Flat | | `sendPaymentRequest` | `messages.sendPaymentRequest` | No | Namespaced | | `sendPoll` | `messages.sendPoll` | No | Namespaced | | `sendPtt` | `messages.sendPtt` | No | Namespaced | | `sendPtt` | `messages.sendVoiceNote` | No | Namespaced | | `sendPtt` | `sendVoiceNote` | No | Flat | | `sendRawMessage` | `messages.sendRaw` | No | Namespaced | | `sendReplyWithMentions` | `messages.sendReplyWithMentions` | No | Namespaced | | `sendSeen` | `messages.sendSeen` | No | Namespaced | | `sendSticker` | `messages.sendSticker` | No | Namespaced | | `sendText` | `messages.sendText` | No | Namespaced | | `sendTextWithMentions` | `messages.sendTextWithMentions` | No | Namespaced | | `sendVCard` | `messages.sendVCard` | No | Namespaced | | `sendVideoAsGif` | `messages.sendVideoAsGif` | No | Namespaced | | `sendYouTubeLink` | `messages.sendYoutubeLink` | Yes | Namespaced | | `sendYouTubeLink` | `messages.sendYouTubeLink` | No | Namespaced | | `sendYouTubeLink` | `sendYoutubeLink` | Yes | Flat | | `setCallMedia` | `calls.setCallMedia` | No | Namespaced | | `setChatEphemeral` | `chats.setEphemeral` | No | Namespaced | | `setGroupDescription` | `groups.setDescription` | No | Namespaced | | `setGroupEditToAdminsOnly` | `groups.setEditAdminsOnly` | No | Namespaced | | `setGroupEditToAdminsOnly` | `groups.setEditToAdminsOnly` | No | Namespaced | | `setGroupIcon` | `groups.setIcon` | No | Namespaced | | `setGroupTitle` | `groups.setTitle` | No | Namespaced | | `setGroupToAdminsOnly` | `groups.setAdminsOnly` | No | Namespaced | | `setGroupToAdminsOnly` | `groups.setToAdminsOnly` | No | Namespaced | | `setMyName` | `session.setDisplayName` | No | Namespaced | | `setMyName` | `session.setMyName` | No | Namespaced | | `setMyName` | `session.setName` | No | Namespaced | | `setMyName` | `setDisplayName` | No | Flat | | `setMyStatus` | `session.setMyStatus` | No | Namespaced | | `setMyStatus` | `session.setStatus` | No | Namespaced | | `setMyStatus` | `session.setStatusText` | No | Namespaced | | `setMyStatus` | `setStatusText` | No | Flat | | `setProfilePicture` | `session.setProfilePic` | Yes | Namespaced | | `setProfilePicture` | `session.setProfilePicture` | No | Namespaced | | `setProfilePicture` | `setProfilePic` | Yes | Flat | | `sharePhoneNumber` | `contacts.sharePhoneNumber` | No | Namespaced | | `starMessage` | `messages.star` | No | Namespaced | | `startCall` | `calls.startCall` | No | Namespaced | | `stopCallAudioObserver` | `calls.stopCallAudioObserver` | No | Namespaced | | `unarchiveChat` | `chats.unarchive` | No | Namespaced | | `unblockContact` | `contacts.contactUnblock` | Yes | Namespaced | | `unblockContact` | `contacts.unblock` | No | Namespaced | | `unblockContact` | `contactUnblock` | Yes | Flat | | `unmuteChat` | `chats.unmute` | No | Namespaced | | `unpinChat` | `chats.unpin` | No | Namespaced | | `unpinMessage` | `messages.unpin` | No | Namespaced | | `unstarMessage` | `messages.unstar` | No | Namespaced | | `voteOnPoll` | `messages.voteOnPoll` | No | Namespaced | ## Parameter key aliases [#parameter-key-aliases] Alternative object keys accepted for a canonical parameter. | Canonical key | Alias | Branded type | Example | | ---------------- | ------------- | ------------- | --------------------------------------------------------------------- | | `chatId` | `chatId` | `ChatId` | `"447123456789@c.us"` | | `chatId` | `contactId` | `ChatId` | `"447123456789@c.us"` | | `chatId` | `id` | `ChatId` | `"447123456789@c.us"` | | `contactId` | `id` | `ContactId` | `"447123456789@c.us"` | | `contactIds` | `id` | `ContactId` | `"447123456789@c.us"` | | `contacts` | `id` | `ContactId` | `"447123456789@c.us"` | | `content` | `body` | `Content` | `"Hello, world!"` | | `content` | `message` | `Content` | `"Hello, world!"` | | `content` | `text` | `Content` | `"Hello, world!"` | | `eventId` | `id` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `eventId` | `msgId` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `groupId` | `chatId` | `GroupChatId` | `"447123456789-1445627445@g.us"` | | `groupId` | `groupChatId` | `GroupChatId` | `"447123456789-1445627445@g.us"` | | `messageId` | `id` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `messageId` | `messages` | `MessageId[]` | `["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"]` | | `messageId` | `msgId` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `messages` | `messages` | `MessageId[]` | `["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"]` | | `name` | `newName` | - | `"OpenWA Bot"` | | `quotedMsgId` | `id` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `quotedMsgId` | `msgId` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `replyMessageId` | `id` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `replyMessageId` | `msgId` | `MessageId` | `"false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"` | | `statusText` | `newStatus` | - | `"Available"` | | `text` | `body` | `Content` | `"Hello, world!"` | | `text` | `message` | `Content` | `"Hello, world!"` | | `text` | `text` | `Content` | `"Hello, world!"` | | `to` | `chatId` | `ChatId` | `"447123456789@c.us"` | | `userAgent` | `userA` | - | `"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"` | # Argument normalization (/docs/reference/client/argument-normalization) # Argument normalization [#argument-normalization] Every generated method accepts either positional arguments (mapped by `parameterOrder`) or a single options object (with key aliases resolved). ```ts await client.sendText('447123456789@c.us', 'Hello, world!'); // positional args map by parameterOrder -> { to, content, options } await client.sendText({ chatId: '447123456789@c.us', text: 'Hello, world!' }); // key aliases normalize -> { to, content } ``` Steps: 1) resolve positional/object args, 2) normalize key aliases, 3) `inputSchema.parseAsync()` validates, 4) dispatch to `execute(...)`. Invalid input rejects before any WhatsApp call. # BaseClient (/docs/reference/client/base-client) # BaseClient [#baseclient] `BaseClient` is generated from the registry. Each method is a one-line binding, for example: ```ts /** Sends a text message to a chat */ public sendText = implementMethod(Methods.sendText); ``` At runtime each call normalizes arguments, validates them with the method's Zod input schema, then dispatches to `execute(meta.wapiOverride ?? meta.functionName, validatedParams)` (falling back to `pup()` page evaluation). See [Argument normalization](/docs/reference/client/argument-normalization). ## Method dispatch table [#method-dispatch-table] | Method | Namespace | Dispatch target | Parameter order | Output | | ---------------------------- | ------------- | ----------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `acceptCall` | `calls` | `acceptCall` | `call`, `media` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `addLabel` | `labels` | `addLabel` | `label`, `chatId` | `boolean` | | `addParticipant` | `groups` | `addParticipant` | `groupId`, `contactId` | `boolean` | | `approveGroupJoinRequest` | `groups` | `approveGroupJoinRequest` | `groupId`, `contactId` | `string \| boolean` | | `archiveChat` | `chats` | `archiveChat` | `chatId` | `boolean` | | `blockContact` | `contacts` | `contactBlock` | `contactId` | `boolean` | | `cancelEvent` | `events` | `cancelEvent` | `eventId` | `boolean` | | `checkNumberStatus` | `contacts` | `checkNumberStatus` | `contactId` | `NumberCheck` | | `checkReadReceipts` | `contacts` | `checkReadReceipts` | `contactId` | `boolean \| string` | | `clearCallAudio` | `calls` | `clearCallAudio` | `call` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `clearChat` | `chats` | `clearChat` | `chatId` | `boolean` | | `createGroup` | `groups` | `createGroup` | `name`, `contacts` | `GroupCreationResult \| null` | | `decryptMedia` | `media` | `decryptMedia` | `message` | `DataURL` | | `deleteAllStatus` | `status` | `deleteAllStatus` | - | `boolean` | | `deleteChat` | `chats` | `deleteChat` | `chatId` | `boolean` | | `deleteMessage` | `messages` | `deleteMessage` | `chatId`, `messageId`, `onlyLocal` | `boolean` | | `deleteStatus` | `status` | `deleteStatus` | `statusesToDelete` | `boolean` | | `demoteParticipant` | `groups` | `demoteParticipant` | `groupId`, `contactId` | `boolean \| string` | | `downloadMedia` | `media` | `downloadMedia` | `message`, `path` | `string` | | `editEvent` | `events` | `editEvent` | `eventId`, `changes` | `boolean` | | `editMessage` | `messages` | `editMessage` | `messageId`, `newContent` | `boolean` | | `endCall` | `calls` | `endCall` | `call` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `forwardMessages` | `messages` | `forwardMessages` | `to`, `messages`, `skipMyMessages` | `MessageIdReturn[] \| boolean` | | `getActiveCall` | `calls` | `getActiveCall` | - | `{ id: string; sessionId: string; generation: string; peer: string; state: string; direction: "incoming" \| "outgoing"; video: boolean; terminal: boolean; observedAt: number \| "Infinity" \| "-Infinity" \| "NaN"; endReason?: { code: string; message: string; source: "local" \| "media" \| "license" \| "session" \| "whatsapp" } \| null; media?: { state: "attached" \| "ended"; reason?: string \| null } \| null` | | `getAlbumMessages` | `messages` | `getAlbumMessages` | `messageId` | `Message[] \| false` | | `getAllChatIds` | `chatIds` | `getAllChatIds` | - | `ChatId[]` | | `getAllChats` | `chats` | `getAllChats` | `withNewMessagesOnly` | `Chat[]` | | `getAllCommunities` | `communities` | `getAllCommunities` | - | `GroupChatId[]` | | `getAllContacts` | `contacts` | `getAllContacts` | - | `Contact[]` | | `getAllGroups` | `groups` | `getAllGroups` | `withNewMessagesOnly` | `Chat[]` | | `getAllLabels` | `labels` | `getAllLabels` | - | `Label[]` | | `getAllMessages` | `messages` | `getAllMessages` | `chatId`, `includeMe`, `includeNotifications` | `Message[]` | | `getBatteryLevel` | `session` | `getBatteryLevel` | - | `number` | | `getBlockedIds` | `contacts` | `getBlockedIds` | - | `ContactId[] \| false` | | `getBusinessProducts` | `business` | `getBusinessProfilesProducts` | `contactId` | `Product[] \| false` | | `getBusinessProfile` | `business` | `getBusinessProfile` | `contactId` | `BusinessProfile` | | `getCallCapabilities` | `calls` | `getCallCapabilities` | - | `{ control: boolean; audio: boolean; video: boolean; reason?: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" } }` | | `getChat` | `chats` | `getChat` | `chatId` | `Chat \| null \| undefined \| false` | | `getChatsByLabel` | `labels` | `getChatsByLabel` | `label` | `Chat[]` | | `getChatWithNonContacts` | `chats` | `getChatWithNonContacts` | - | `Contact[]` | | `getCommonGroups` | `contacts` | `getCommonGroups` | `contactId` | `CommonGroup[] \| false` | | `getCommunityAdminIds` | `communities` | `getCommunityAdminIds` | `communityId` | `CommunityAdminIds[]` | | `getCommunityAdmins` | `communities` | `getCommunityAdmins` | `communityId` | `CommunityAdmins[]` | | `getCommunityInfo` | `communities` | `getCommunityInfo` | `communityId` | `CommunityMetadata` | | `getCommunityParticipantIds` | `communities` | `getCommunityParticipantIds` | `communityId` | `CommunityParticipantIds[]` | | `getCommunityParticipants` | `communities` | `getCommunityParticipants` | `communityId` | `CommunityParticipants[]` | | `getConnectionState` | `session` | `getConnectionState` | - | `string` | | `getContact` | `contacts` | `getContact` | `contactId` | `Contact \| null` | | `getEvent` | `events` | `getEvent` | `eventId` | `WhatsAppEvent \| null` | | `getEventResponses` | `events` | `getEventResponses` | `eventId` | `EventResponse[]` | | `getEvents` | `events` | `getEvents` | `chatId`, `options` | `EventPage` | | `getFeatures` | `session` | `getFeatures` | - | `FeatureFlags` | | `getGroupAdmins` | `groups` | `getGroupAdmins` | `groupId` | `ContactId[]` | | `getGroupInfo` | `groups` | `getGroupInfo` | `groupId` | `{ title: string; description?: string \| null } \| string \| null` | | `getGroupInviteLink` | `groups` | `getGroupInviteLink` | `groupId` | `string` | | `getGroupMembers` | `groups` | `getGroupMembers` | `groupId` | `Contact[]` | | `getGroupMembersId` | `groups` | `getGroupMembersId` | `groupId` | `ContactId[]` | | `getHostNumber` | `session` | `getHostNumber` | - | `string` | | `getKickedGroups` | `groups` | `getKickedGroups` | - | `GroupChatId[]` | | `getLicenseFeatures` | `session` | `getLicenseFeatures` | - | `{ status: "valid" \| "missing" \| "invalid" \| "expired" \| "metadata_only"; keyType: string \| null; expiresAt: number \| "Infinity" \| "-Infinity" \| "NaN" \| null; features: Record }` | | `getLicenseType` | `session` | `getLicenseType` | - | `LicenseType \| false` | | `getLoadedMessageCount` | `session` | `getAmountOfLoadedMessages` | - | `number` | | `getMe` | `session` | `getMe` | - | `HostAccount` | | `getMessageById` | `messages` | `getMessageById` | `messageId` | `Message \| null \| false` | | `getMessageInfo` | `messages` | `getMessageInfo` | `messageId` | `MessageInfo` | | `getMessagesForLLM` | `messages` | `getGptArray` | `chatId`, `last` | `LLMMessage[]` | | `getMyLastMessage` | `messages` | `getMyLastMessage` | `chatId` | `Message \| undefined` | | `getNumberProfile` | `contacts` | `getNumberProfile` | `contactId` | `NumberProfile` | | `getOrder` | `business` | `getOrder` | `id` | `Order` | | `getPollResults` | `messages` | `getPollResults` | `messageId` | `PollResults` | | `getProcessStats` | `session` | `getProcessStats` | - | `ProcessStats` | | `getProfilePicture` | `contacts` | `getProfilePicFromServer` | `chatId` | `string` | | `getSnapshot` | `session` | `getSnapshot` | `chatId`, `width`, `height` | `string` | | `getStarredMessages` | `messages` | `getStarredMessages` | `chatId` | `Message[]` | | `getStatus` | `mystatus` | `getStatus` | `contactId` | `ContactStatus` | | `getStories` | `status` | `getStories` | - | `Story[]` | | `getUnsentMessages` | `messages` | `getUnsentMessages` | - | `Message[]` | | `getUserAgent` | `session` | `getGeneratedUserAgent` | `userAgent` | `string` | | `getVCards` | `messages` | `getVCards` | `messageId` | `string[]` | | `getWAVersion` | `session` | `getWAVersion` | - | `string` | | `healthCheck` | `session` | `healthCheck` | - | `HealthCheck` | | `isChatOnline` | `chats` | `isChatOnline` | `chatId` | `boolean \| string` | | `isPlugged` | `session` | `getIsPlugged` | - | `boolean` | | `joinGroupViaLink` | `groups` | `joinGroupViaLink` | `link`, `returnChatObj` | `GroupChatId \| boolean \| number \| Chat` | | `leaveGroup` | `groups` | `leaveGroup` | `groupId` | `void` | | `loadEarlierMessages` | `messages` | `loadEarlierMessages` | `chatId`, `count`, `includeMe` | `Message[]` | | `markAsRead` | `chats` | `markAsRead` | `chatId` | `boolean` | | `markAsUnread` | `chats` | `markAsUnread` | `chatId` | `boolean` | | `muteCall` | `calls` | `muteCall` | `call`, `muted` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `muteChat` | `chats` | `muteChat` | `chatId`, `muteDuration` | `string \| boolean \| number` | | `observeCallAudio` | `calls` | `observeCallAudio` | `call`, `speaker` | `{ ok: true; status: "requested"; observerId: string } \| { ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `pinChat` | `chats` | `pinChat` | `chatId` | `boolean` | | `pinMessage` | `messages` | `pinMessage` | `messageId`, `duration` | `boolean` | | `postImageStatus` | `status` | `postImageStatus` | `image`, `caption` | `MessageId \| string \| boolean` | | `postTextStatus` | `status` | `postTextStatus` | `text`, `backgroundColor`, `font` | `MessageId \| string \| boolean` | | `postVideoStatus` | `status` | `postVideoStatus` | `video`, `caption` | `MessageId \| string \| boolean` | | `promoteParticipant` | `groups` | `promoteParticipant` | `groupId`, `contactId` | `boolean \| string` | | `react` | `messages` | `react` | `messageId`, `emoji` | `boolean` | | `rejectCall` | `calls` | `rejectCall` | `call` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `rejectGroupJoinRequest` | `groups` | `rejectGroupJoinRequest` | `groupId`, `contactId` | `string \| boolean` | | `removeLabel` | `labels` | `removeLabel` | `label`, `chatId` | `boolean` | | `removeParticipant` | `groups` | `removeParticipant` | `groupId`, `contactId` | `boolean` | | `requestPhoneNumber` | `contacts` | `requestPhoneNumber` | `to` | `string` | | `respondToEvent` | `events` | `respondToEvent` | `eventId`, `response` | `boolean` | | `revokeGroupInviteLink` | `groups` | `revokeGroupInviteLink` | `groupId` | `string \| boolean` | | `sendAdvancedButtons` | `messages` | `sendAdvancedButtons` | `to`, `body`, `buttons`, `text`, `footer`, `filename` | `MessageIdReturn \| boolean` | | `sendAlbum` | `messages` | `sendAlbum` | `to`, `media`, `caption`, `quotedMsgId` | `AlbumSendResult` | | `sendAudio` | `messages` | `sendAudio` | `to`, `file`, `filename`, `options` | `MessageIdReturn \| boolean` | | `sendBanner` | `messages` | `sendBanner` | `to`, `base64` | `MessageIdReturn \| boolean` | | `sendButtons` | `messages` | `sendButtons` | `to`, `body`, `buttons`, `title`, `footer` | `MessageIdReturn \| boolean` | | `sendContact` | `messages` | `sendContact` | `to`, `contactId` | `MessageIdReturn \| boolean \| (MessageIdReturn \| boolean)[]` | | `sendEvent` | `events` | `sendEvent` | `to`, `event` | `string` | | `sendFile` | `messages` | `sendFile` | `to`, `file`, `filename`, `options` | `MessageIdReturn \| boolean` | | `sendFileFromUrl` | `messages` | `sendFileFromUrl` | `to`, `url`, `filename`, `options`, `headers` | `MessageIdReturn \| string \| boolean` | | `sendImage` | `messages` | `sendImage` | `to`, `file`, `filename`, `options` | `MessageIdReturn \| boolean` | | `sendInteractive` | `messages` | `sendInteractive` | `to`, `content` | `string` | | `sendLinkWithAutoPreview` | `messages` | `sendLinkWithAutoPreview` | `to`, `url`, `text`, `thumbnail`, `quotedMsgId`, `customSize` | `MessageIdReturn \| boolean` | | `sendListMessage` | `messages` | `sendListMessage` | `to`, `sections`, `title`, `description`, `actionText` | `MessageIdReturn \| boolean` | | `sendLocation` | `messages` | `sendLocation` | `to`, `lat`, `lng`, `loc`, `address`, `url` | `MessageIdReturn \| boolean` | | `sendMessageWithThumb` | `messages` | `sendMessageWithThumb` | `thumb`, `url`, `title`, `description`, `content`, `to`, `quotedMsgId`, `customSize` | `MessageIdReturn \| boolean` | | `sendMultipleContacts` | `messages` | `sendMultipleContacts` | `to`, `contactIds` | `MessageIdReturn \| boolean` | | `sendPaymentRequest` | `messages` | `sendPaymentRequest` | `to`, `amount`, `currency`, `message` | `MessageIdReturn \| boolean` | | `sendPoll` | `messages` | `sendPoll` | `to`, `name`, `options`, `config` | `MessageIdReturn \| boolean` | | `sendPtt` | `messages` | `sendPtt` | `to`, `file`, `options` | `MessageIdReturn \| boolean` | | `sendRawMessage` | `messages` | `sendRawMessage` | `to`, `payload` | `string` | | `sendReplyWithMentions` | `messages` | `sendReplyWithMentions` | `to`, `content`, `replyMessageId`, `hideTags`, `mentions` | `MessageIdReturn \| boolean` | | `sendSeen` | `messages` | `sendSeen` | `chatId` | `boolean` | | `sendSticker` | `messages` | `sendStickerJob` | `to`, `input`, `job` | `MessageId` | | `sendText` | `messages` | `sendText` | `to`, `content`, `options` | `MessageId` | | `sendTextWithMentions` | `messages` | `sendTextWithMentions` | `to`, `content`, `hideTags`, `mentions` | `MessageIdReturn \| boolean` | | `sendVCard` | `messages` | `sendVCard` | `to`, `vcard`, `contactName`, `contactNumber` | `MessageIdReturn \| boolean` | | `sendVideoAsGif` | `messages` | `sendVideoAsGif` | `to`, `file`, `filename`, `options` | `MessageIdReturn \| boolean` | | `sendYouTubeLink` | `messages` | `sendYoutubeLink` | `to`, `url`, `text`, `thumbnail`, `quotedMsgId`, `customSize` | `MessageIdReturn \| boolean` | | `setCallMedia` | `calls` | `setCallMedia` | `call`, `media` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `setChatEphemeral` | `chats` | `setChatEphemeral` | `chatId`, `ephemeral` | `boolean` | | `setGroupDescription` | `groups` | `setGroupDescription` | `groupId`, `description` | `boolean` | | `setGroupEditToAdminsOnly` | `groups` | `setGroupEditToAdminsOnly` | `groupId`, `onlyAdmins` | `boolean` | | `setGroupIcon` | `groups` | `setGroupIcon` | `groupId`, `image` | `boolean` | | `setGroupTitle` | `groups` | `setGroupTitle` | `groupId`, `title` | `boolean` | | `setGroupToAdminsOnly` | `groups` | `setGroupToAdminsOnly` | `groupId`, `onlyAdmins` | `boolean` | | `setMyName` | `session` | `setMyName` | `name` | `boolean` | | `setMyStatus` | `session` | `setMyStatus` | `statusText` | `boolean` | | `setProfilePicture` | `session` | `setProfilePic` | `image` | `boolean` | | `sharePhoneNumber` | `contacts` | `sharePhoneNumber` | `to` | `string` | | `starMessage` | `messages` | `starMessage` | `messageId` | `boolean` | | `startCall` | `calls` | `startCall` | `to`, `microphone`, `speaker`, `camera` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `stopCallAudioObserver` | `calls` | `stopCallAudioObserver` | `call`, `observerId` | `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string` | | `unarchiveChat` | `chats` | `unarchiveChat` | `chatId` | `boolean` | | `unblockContact` | `contacts` | `contactUnblock` | `contactId` | `boolean` | | `unmuteChat` | `chats` | `unmuteChat` | `chatId` | `string \| boolean \| number` | | `unpinChat` | `chats` | `unpinChat` | `chatId` | `boolean` | | `unpinMessage` | `messages` | `unpinMessage` | `messageId` | `boolean` | | `unstarMessage` | `messages` | `unstarMessage` | `messageId` | `boolean` | | `voteOnPoll` | `messages` | `voteOnPoll` | `messageId`, `optionIds` | `boolean` | # Business Client API (/docs/reference/client/business) # Business Client API [#business-client-api] This page documents 3 schema-registry client methods in the `business` namespace. ## `getBusinessProducts` [#getbusinessproducts] Get business products **Signature** `client.getBusinessProducts(contactId: string): Promise` ### Usage [#usage] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBusinessProducts({ contactId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/business/getProducts" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Product | [View fields](/docs/reference/client/return-types#product) | Resolves to `Product[] \| false`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------------------- | | Namespace | `business` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `business.getProducts`, `business.getProfilesProducts`, `getBusinessProfilesProducts` | | Deprecated aliases | `business.getProfilesProducts`, `getBusinessProfilesProducts` | | License | `none` | | WAPI override | `getBusinessProfilesProducts` | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ---------------- | ------ | ----------------------------------- | ------------------------------ | -------------- | | Primary | `GET` | `/api/business/getProducts` | `getBusinessProducts` | Active | | Deprecated alias | `GET` | `/api/business/getProfilesProducts` | `business.getProfilesProducts` | **Deprecated** | | Alias | `GET` | `/api/getBusinessProducts` | `getBusinessProducts` | Active | | Deprecated alias | `GET` | `/api/getBusinessProfilesProducts` | `getBusinessProfilesProducts` | **Deprecated** |
## `getBusinessProfile` [#getbusinessprofile] Get business profile **Signature** `client.getBusinessProfile(contactId: string): Promise` ### Usage [#usage-1] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBusinessProfile({ contactId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/business/getProfile" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": { "id": "447123456789@c.us" } } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | BusinessProfile | [View fields](/docs/reference/client/return-types#businessprofile) | Resolves to `BusinessProfile`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | --------------------- | | Namespace | `business` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `business.getProfile` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | -------------------- | ------ | | Primary | `GET` | `/api/business/getProfile` | `getBusinessProfile` | Active | | Alias | `GET` | `/api/getBusinessProfile` | `getBusinessProfile` | Active |
# Calls Client API (/docs/reference/client/calls) # Calls Client API [#calls-client-api] This page documents 11 schema-registry client methods in the `calls` namespace. ## `getCallCapabilities` [#getcallcapabilities] Optional calling availability for UI and diagnostics. Actions perform admission automatically. **Signature** `client.getCallCapabilities(): Promise<{ control: boolean; audio: boolean; video: boolean; reason?: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" } }>` ### Usage [#usage-4] ```ts const result = await client.getCallCapabilities(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCallCapabilities(); ``` ```bash curl -X POST "http://localhost:8080/api/calls/getCallCapabilities" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": { "control": true, "audio": true, "video": true } } ``` ### Parameters [#parameters-4] This method does not define input parameters. ### Returns [#returns-4] | Prop | Value | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Return type | `Promise<{ control: boolean; audio: boolean; video: boolean; reason?: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" } }>` | Resolves to `{ control: boolean; audio: boolean; video: boolean; reason?: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" } }`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | --------------------------- | | Namespace | `calls` | | Action | `update` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `calls.getCallCapabilities` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------------- | --------------------- | ------ | | Primary | `POST` | `/api/calls/getCallCapabilities` | `getCallCapabilities` | Active | | Alias | `POST` | `/api/getCallCapabilities` | `getCallCapabilities` | Active |
# ChatIds Client API (/docs/reference/client/chatids) # ChatIds Client API [#chatids-client-api] This page documents 1 schema-registry client methods in the `chatIds` namespace. ## `getAllChatIds` [#getallchatids] Get all chat IDs **Signature** `client.getAllChatIds(): Promise` ### Usage [#usage] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllChatIds(); ``` ```bash curl -X GET "http://localhost:8080/api/chatIds/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters] This method does not define input parameters. ### Returns [#returns] | Prop | Value | | ----------- | --------------------------------------------------------- | | Return type | `Promise` | | ChatId | [View fields](/docs/reference/client/return-types#chatid) | Resolves to `ChatId[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Opaque conversation identifier. Use the exact value returned by a message event or chat lookup.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | --------------------------------------------------------------------------- | | Namespace | `chatIds` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `chatIds.all`, `chatIds.getAll`, `chatIds.list`, `chats.ids`, `listChatIds` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | --------------- | ------ | | Primary | `GET` | `/api/chatIds/getAll` | `getAllChatIds` | Active | | Alias | `GET` | `/api/chatIds/all` | `chatIds.all` | Active | | Alias | `GET` | `/api/chatIds/list` | `chatIds.list` | Active | | Alias | `GET` | `/api/chats/ids` | `chats.ids` | Active | | Alias | `GET` | `/api/getAllChatIds` | `getAllChatIds` | Active | | Alias | `GET` | `/api/listChatIds` | `listChatIds` | Active |
# Chats Client API (/docs/reference/client/chats) # Chats Client API [#chats-client-api] This page documents 15 schema-registry client methods in the `chats` namespace. ## `archiveChat` [#archivechat] Archive a chat **Signature** `client.archiveChat(chatId: string): Promise` ### Usage [#usage] ```ts const result = await client.archiveChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.archiveChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/archive" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | --------------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.archive` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | ------------- | ------ | | Primary | `PUT` | `/api/chats/archive` | `archiveChat` | Active | | Alias | `PUT` | `/api/archiveChat` | `archiveChat` | Active |
## `clearChat` [#clearchat] Clear chat history **Signature** `client.clearChat(chatId: string): Promise` ### Usage [#usage-1] ```ts const result = await client.clearChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.clearChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X DELETE "http://localhost:8080/api/chats/clear" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | ------------- | | Namespace | `chats` | | Action | `delete` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.clear` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | -------- | ------------------ | ----------- | ------ | | Primary | `DELETE` | `/api/chats/clear` | `clearChat` | Active | | Alias | `DELETE` | `/api/clearChat` | `clearChat` | Active |
## `deleteChat` [#deletechat] Delete a chat **Signature** `client.deleteChat(chatId: string): Promise` ### Usage [#usage-2] ```ts const result = await client.deleteChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.deleteChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X DELETE "http://localhost:8080/api/chats/delete" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-2] ### Returns [#returns-2] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | -------------- | | Namespace | `chats` | | Action | `delete` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.delete` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | -------- | ------------------- | ------------ | ------ | | Primary | `DELETE` | `/api/chats/delete` | `deleteChat` | Active | | Alias | `DELETE` | `/api/deleteChat` | `deleteChat` | Active |
## `getAllChats` [#getallchats] Returns serialized Chat objects from the current session, optionally limited to chats with unread messages. **Signature** `client.getAllChats(withNewMessagesOnly?: boolean): Promise` ### Usage [#usage-3] ```ts const result = await client.getAllChats(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllChats(); ``` ```bash curl -X GET "http://localhost:8080/api/chats/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-3] ### Returns [#returns-3] | Prop | Value | | ----------- | ------------------------------------------------------- | | Return type | `Promise` | | Chat | [View fields](/docs/reference/client/return-types#chat) | Resolves to `Chat[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Serialized chat returned by chat lookups and listings. Its id is an opaque ChatId; additional WhatsApp Web fields may be present.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | ------------------------------------------------------ | | Namespace | `chats` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `withNewMessagesOnly` | | Aliases | `chats.all`, `chats.getAll`, `chats.list`, `listChats` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------- | ------------- | ------ | | Primary | `GET` | `/api/chats/getAll` | `getAllChats` | Active | | Alias | `GET` | `/api/chats/all` | `chats.all` | Active | | Alias | `GET` | `/api/chats/list` | `chats.list` | Active | | Alias | `GET` | `/api/getAllChats` | `getAllChats` | Active | | Alias | `GET` | `/api/listChats` | `listChats` | Active |
## `getChat` [#getchat] Get specific chat by ID **Signature** `client.getChat(chatId: string): Promise` ### Usage [#usage-4] ```ts const result = await client.getChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/chats/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": { "id": "447123456789@c.us", "isGroup": true, "contact": { "id": "447123456789@c.us" } } } ``` ### Parameters [#parameters-4] ### Returns [#returns-4] | Prop | Value | | ----------- | ------------------------------------------------------- | | Return type | `Promise` | | Chat | [View fields](/docs/reference/client/return-types#chat) | Resolves to `Chat \| null \| undefined \| false`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ------------------------------------------- | | Namespace | `chats` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.get`, `chats.getById`, `getChatById` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | --------------- | ------ | | Primary | `GET` | `/api/chats/get` | `getChat` | Active | | Alias | `GET` | `/api/chats/getById` | `chats.getById` | Active | | Alias | `GET` | `/api/getChat` | `getChat` | Active | | Alias | `GET` | `/api/getChatById` | `getChatById` | Active |
## `getChatWithNonContacts` [#getchatwithnoncontacts] Get chats with non-contacts **Signature** `client.getChatWithNonContacts(): Promise` ### Usage [#usage-5] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getChatWithNonContacts(); ``` ```bash curl -X GET "http://localhost:8080/api/chats/getWithNonContacts" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-5] This method does not define input parameters. ### Returns [#returns-5] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Contact | [View fields](/docs/reference/client/return-types#contact) | Resolves to `Contact[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | -------------------------- | | Namespace | `chats` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `chats.getWithNonContacts` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ------------------------ | ------ | | Primary | `GET` | `/api/chats/getWithNonContacts` | `getChatWithNonContacts` | Active | | Alias | `GET` | `/api/getChatWithNonContacts` | `getChatWithNonContacts` | Active |
## `isChatOnline` [#ischatonline] Check if chat is online **Signature** `client.isChatOnline(chatId: string): Promise` ### Usage [#usage-6] ```ts const result = await client.isChatOnline("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.isChatOnline({ chatId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/chats/isOnline" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-6] ### Returns [#returns-6] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-6] | Prop | Value | | -------------------------- | ---------------- | | Namespace | `chats` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.isOnline` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-6] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | -------------- | ------ | | Primary | `GET` | `/api/chats/isOnline` | `isChatOnline` | Active | | Alias | `GET` | `/api/isChatOnline` | `isChatOnline` | Active |
## `markAsRead` [#markasread] Mark chat as read **Signature** `client.markAsRead(chatId: string): Promise` ### Usage [#usage-7] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.markAsRead({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/markAsRead" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-7] ### Returns [#returns-7] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-7] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.markAsRead` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-7] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ------------ | ------ | | Primary | `PUT` | `/api/chats/markAsRead` | `markAsRead` | Active | | Alias | `PUT` | `/api/markAsRead` | `markAsRead` | Active |
## `markAsUnread` [#markasunread] Mark chat as unread **Signature** `client.markAsUnread(chatId: string): Promise` ### Usage [#usage-8] ```ts const result = await client.markAsUnread("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.markAsUnread({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/markAsUnread" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-8] ### Returns [#returns-8] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-8] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.markAsUnread` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-8] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | -------------- | ------ | | Primary | `PUT` | `/api/chats/markAsUnread` | `markAsUnread` | Active | | Alias | `PUT` | `/api/markAsUnread` | `markAsUnread` | Active |
## `muteChat` [#mutechat] Mute a chat **Signature** `client.muteChat(chatId: string, muteDuration: number \| string): Promise` ### Usage [#usage-9] ```ts const result = await client.muteChat("447123456789@c.us", 1); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.muteChat({ chatId: "447123456789@c.us", muteDuration: 1, }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/mute" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us", "muteDuration": 1 }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-9] ### Returns [#returns-9] | Prop | Value | | ----------- | -------------------------------------- | | Return type | `Promise` | Resolves to `string \| boolean \| number`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-9] | Prop | Value | | -------------------------- | ------------------------ | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId`, `muteDuration` | | Aliases | `chats.mute` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-9] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------- | ---------- | ------ | | Primary | `PUT` | `/api/chats/mute` | `muteChat` | Active | | Alias | `PUT` | `/api/muteChat` | `muteChat` | Active |
## `pinChat` [#pinchat] Pin a chat **Signature** `client.pinChat(chatId: string): Promise` ### Usage [#usage-10] ```ts const result = await client.pinChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.pinChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/pin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-10] ### Returns [#returns-10] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-10] | Prop | Value | | -------------------------- | ----------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.pin` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-10] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------- | --------- | ------ | | Primary | `PUT` | `/api/chats/pin` | `pinChat` | Active | | Alias | `PUT` | `/api/pinChat` | `pinChat` | Active |
## `setChatEphemeral` [#setchatephemeral] Set ephemeral messages for chat **Signature** `client.setChatEphemeral(chatId: string, ephemeral: boolean \| number): Promise` ### Usage [#usage-11] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setChatEphemeral({ chatId: "447123456789@c.us", ephemeral: 1, }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/setEphemeral" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us", "ephemeral": 1 }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-11] ### Returns [#returns-11] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-11] | Prop | Value | | -------------------------- | --------------------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId`, `ephemeral` | | Aliases | `chats.setEphemeral` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-11] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ------------------ | ------ | | Primary | `PUT` | `/api/chats/setEphemeral` | `setChatEphemeral` | Active | | Alias | `PUT` | `/api/setChatEphemeral` | `setChatEphemeral` | Active |
## `unarchiveChat` [#unarchivechat] Unarchive a chat **Signature** `client.unarchiveChat(chatId: string): Promise` ### Usage [#usage-12] ```ts const result = await client.unarchiveChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unarchiveChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/unarchive" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-12] ### Returns [#returns-12] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-12] | Prop | Value | | -------------------------- | ----------------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.unarchive` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-12] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------- | --------------- | ------ | | Primary | `PUT` | `/api/chats/unarchive` | `unarchiveChat` | Active | | Alias | `PUT` | `/api/unarchiveChat` | `unarchiveChat` | Active |
## `unmuteChat` [#unmutechat] Unmute a chat **Signature** `client.unmuteChat(chatId: string): Promise` ### Usage [#usage-13] ```ts const result = await client.unmuteChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unmuteChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/unmute" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-13] ### Returns [#returns-13] | Prop | Value | | ----------- | -------------------------------------- | | Return type | `Promise` | Resolves to `string \| boolean \| number`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-13] | Prop | Value | | -------------------------- | -------------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.unmute` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-13] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------- | ------------ | ------ | | Primary | `PUT` | `/api/chats/unmute` | `unmuteChat` | Active | | Alias | `PUT` | `/api/unmuteChat` | `unmuteChat` | Active |
## `unpinChat` [#unpinchat] Unpin a chat **Signature** `client.unpinChat(chatId: string): Promise` ### Usage [#usage-14] ```ts const result = await client.unpinChat("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unpinChat({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/chats/unpin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-14] ### Returns [#returns-14] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-14] | Prop | Value | | -------------------------- | ------------- | | Namespace | `chats` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `chats.unpin` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-14] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------ | ----------- | ------ | | Primary | `PUT` | `/api/chats/unpin` | `unpinChat` | Active | | Alias | `PUT` | `/api/unpinChat` | `unpinChat` | Active |
# Client API (/docs/reference/client/client) Connect a SocketClient to the Easy API, or create an in-process Client before using these methods. ## acceptCall Prepare media and accept this incoming call. Canonical reference: /docs/reference/client/client#acceptcall License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: - `media?: { camera?: any; microphone?: any; onMediaFailure?: "end" | "keep-open"; speaker?: any; takeover?: boolean }`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.acceptCall({ call: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/acceptCall" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {} }' ``` Aliases: calls.acceptCall ## addLabel Add label to chat Canonical reference: /docs/reference/client/client#addlabel ### Parameters - `label: string`: Label ID or name - `chatId: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.addLabel({ label: "label", chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/labels/add" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "label": "label", "chatId": "447123456789@c.us" }' ``` Aliases: labels.add ## addParticipant Add participant to group Canonical reference: /docs/reference/client/client#addparticipant ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.addParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/addParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Aliases: addGroupParticipant, groups.addParticipant ## approveGroupJoinRequest Approve group join request Canonical reference: /docs/reference/client/client#approvegroupjoinrequest ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `string \| boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.approveGroupJoinRequest({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/approveJoinRequest" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Aliases: groups.approveJoinRequest ## archiveChat Archive a chat Canonical reference: /docs/reference/client/client#archivechat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.archiveChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/archive" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.archive ## blockContact Block a contact Canonical reference: /docs/reference/client/client#blockcontact ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.blockContact({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/contacts/block" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "contactId": "447123456789@c.us" }' ``` Aliases: contactBlock, contacts.block, contacts.contactBlock ## cancelEvent Cancel an event created by this account using WhatsApp’s encrypted event edit action. Canonical reference: /docs/reference/client/client#cancelevent ### Parameters - `eventId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.cancelEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/events/cancelEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "eventId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: events.cancelEvent ## checkNumberStatus Check if number is on WhatsApp Canonical reference: /docs/reference/client/client#checknumberstatus License: insiders ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `NumberCheck`. A rejected Promise indicates input validation or dispatch failure. [NumberCheck](/docs/reference/client/return-types#numbercheck) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.checkNumberStatus({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/contacts/checkNumberStatus" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: contacts.checkNumberStatus ## checkReadReceipts Check read receipts setting Canonical reference: /docs/reference/client/client#checkreadreceipts ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.checkReadReceipts({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/contacts/checkReadReceipts" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: contacts.checkReadReceipts, contacts.getReadReceipts, getReadReceipts ## clearCallAudio Discard queued outgoing audio. Canonical reference: /docs/reference/client/client#clearcallaudio License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.clearCallAudio({ call: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/clearCallAudio" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {} }' ``` Aliases: calls.clearCallAudio ## clearChat Clear chat history Canonical reference: /docs/reference/client/client#clearchat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.clearChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X DELETE "http://localhost:8080/api/chats/clear" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.clear ## createGroup Create new group Canonical reference: /docs/reference/client/client#creategroup License: restricted ### Parameters - `name: string`: Group name - `contacts: string[]`: Contact IDs to add A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `GroupCreationResult \| null`. A rejected Promise indicates input validation or dispatch failure. [GroupId](/docs/reference/client/return-types#groupid), [GroupCreationResult](/docs/reference/client/return-types#groupcreationresult) In-process Client: `Promise<{ gid: GroupId } | null>`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.createGroup({ name: "name", contacts: ["447123456789@c.us"], }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/groups/create" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "name": "name", "contacts": [ "447123456789@c.us" ] }' ``` Aliases: groups.create ## decryptMedia Decrypts media from a message Canonical reference: /docs/reference/client/client#decryptmedia ### Parameters - `message: { ack: -1 | 0 | 1 | 2 | 3 | 4; associationType?: string; author?: string; body: string; broadcast?: boolean; caption?: string; chat: { ack?: any; contact: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; formattedTitle?: string; groupMetadata?: { announce?: boolean; creation: number; desc?: string; descOwner?: string; descTime?: number; id: string; owner?: string; participants: { id: string; isAdmin: boolean; isSuperAdmin?: boolean }[]; restrict?: boolean; subject: string }; id: string; isGroup: boolean; isReadOnly?: boolean; lastReceivedKey?: any; msgs?: any[]; muteExpiration?: number; name?: string; notSpam?: boolean; pin?: number; presence?: object; t?: number; unreadCount?: number }; chatId: string; clientUrl?: string; content: string; deprecatedMms3Url?: string; event?: { chatId: string; creatorId: string; description?: string; endTime?: number; extraGuestsAllowed?: boolean; fromMe: boolean; id: string; isCanceled: boolean; isScheduledCall?: boolean; joinLink?: string; location?: { latitude?: number | "Infinity" | "-Infinity" | "NaN"; longitude?: number | "Infinity" | "-Infinity" | "NaN"; name?: string }; name: string; startTime: number; timestamp: number }; expectedImageCount?: number; expectedVideoCount?: number; from: string; fromMe: boolean; id: string; invis?: boolean; isForwarded?: boolean; isGif?: boolean; isGroupMsg: boolean; isMedia: boolean; isMMS?: boolean; isNewMsg?: boolean; isNotification: boolean; isPSA?: boolean; isQuotedMsgAvailable: boolean; labels?: string[]; mentionedJidList?: string[]; notifyName?: string; parentMsgKey?: string; quotedMsg?: any; quotedMsgObj?: any; recvFresh?: boolean; self: "in" | "out"; sender: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; senderId?: string; star?: boolean; t: number; timestamp: number; to: string; type: "chat" | "audio" | "ptt" | "image" | "video" | "album" | "event_creation" | "event_response" | "document" | "sticker" | "location" | "vcard" | "multi_vcard" | "revoked" | "order" | "buttons_response" | "list_response" | "unknown" }`: Message object containing media ### Returns `Promise` Resolves to `DataURL`. A rejected Promise indicates input validation or dispatch failure. [DataURL](/docs/reference/client/return-types#dataurl) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); // Use the full media message received by your message handler. const result = await client.decryptMedia({ message }); ``` ### HTTP ```bash # Save the full incoming media message as the "message" field in request.json. # For downloadMedia, also set "path" to the destination filename. curl -X POST "http://localhost:8080/api/media/decrypt" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data-binary @request.json ``` Aliases: media.decrypt ## deleteAllStatus Delete all statuses Canonical reference: /docs/reference/client/client#deleteallstatus License: insiders ### Parameters ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.deleteAllStatus(); ``` ### HTTP ```bash curl -X DELETE "http://localhost:8080/api/status/deleteAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: deleteAllStatuses, status.deleteAll ## deleteChat Delete a chat Canonical reference: /docs/reference/client/client#deletechat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.deleteChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X DELETE "http://localhost:8080/api/chats/delete" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.delete ## deleteMessage Deletes a message Canonical reference: /docs/reference/client/client#deletemessage ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `messageId: string[] | string`: Message ID(s) to process Branded type: MessageId[] - `onlyLocal?: boolean`: Delete only locally ### Returns `Promise` Resolves to `boolean`: `true` means the runtime accepted the operation and `false` means it did not complete. Provider-side revoke can still be rejected, so the value is not proof that every recipient removed the message. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.deleteMessage({ chatId: "447123456789@c.us", messageId: ["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"], }); ``` ### HTTP ```bash curl -X DELETE "http://localhost:8080/api/messages/delete" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us", "messageId": [ "false_447123456789@c.us_ABC123", "false_447123456789@c.us_DEF456" ] }' ``` Aliases: messages.delete ## deleteStatus Delete status Canonical reference: /docs/reference/client/client#deletestatus License: insiders ### Parameters - `statusesToDelete: string | string[]`: Status ID(s) ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.deleteStatus({ statusesToDelete: [], }); ``` ### HTTP ```bash curl -X DELETE "http://localhost:8080/api/status/delete" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "statusesToDelete": [] }' ``` Aliases: status.delete ## demoteParticipant Demote participant from admin Canonical reference: /docs/reference/client/client#demoteparticipant ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.demoteParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/demoteParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Aliases: demoteGroupParticipant, groups.demoteParticipant ## downloadMedia Downloads and decrypts media from a message to a file Canonical reference: /docs/reference/client/client#downloadmedia ### Parameters - `message: { ack: -1 | 0 | 1 | 2 | 3 | 4; associationType?: string; author?: string; body: string; broadcast?: boolean; caption?: string; chat: { ack?: any; contact: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; formattedTitle?: string; groupMetadata?: { announce?: boolean; creation: number; desc?: string; descOwner?: string; descTime?: number; id: string; owner?: string; participants: { id: string; isAdmin: boolean; isSuperAdmin?: boolean }[]; restrict?: boolean; subject: string }; id: string; isGroup: boolean; isReadOnly?: boolean; lastReceivedKey?: any; msgs?: any[]; muteExpiration?: number; name?: string; notSpam?: boolean; pin?: number; presence?: object; t?: number; unreadCount?: number }; chatId: string; clientUrl?: string; content: string; deprecatedMms3Url?: string; event?: { chatId: string; creatorId: string; description?: string; endTime?: number; extraGuestsAllowed?: boolean; fromMe: boolean; id: string; isCanceled: boolean; isScheduledCall?: boolean; joinLink?: string; location?: { latitude?: number | "Infinity" | "-Infinity" | "NaN"; longitude?: number | "Infinity" | "-Infinity" | "NaN"; name?: string }; name: string; startTime: number; timestamp: number }; expectedImageCount?: number; expectedVideoCount?: number; from: string; fromMe: boolean; id: string; invis?: boolean; isForwarded?: boolean; isGif?: boolean; isGroupMsg: boolean; isMedia: boolean; isMMS?: boolean; isNewMsg?: boolean; isNotification: boolean; isPSA?: boolean; isQuotedMsgAvailable: boolean; labels?: string[]; mentionedJidList?: string[]; notifyName?: string; parentMsgKey?: string; quotedMsg?: any; quotedMsgObj?: any; recvFresh?: boolean; self: "in" | "out"; sender: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; senderId?: string; star?: boolean; t: number; timestamp: number; to: string; type: "chat" | "audio" | "ptt" | "image" | "video" | "album" | "event_creation" | "event_response" | "document" | "sticker" | "location" | "vcard" | "multi_vcard" | "revoked" | "order" | "buttons_response" | "list_response" | "unknown" }`: Message object containing media - `path: string`: File path to save the media ### Returns `Promise` Path to the saved file In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); // Use the full media message received by your message handler. const result = await client.downloadMedia({ message, path: "./downloaded-media" }); ``` ### HTTP ```bash # Save the full incoming media message as the "message" field in request.json. # For downloadMedia, also set "path" to the destination filename. curl -X POST "http://localhost:8080/api/media/download" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data-binary @request.json ``` Aliases: media.download ## editEvent Edit an event created by this account. Unspecified fields retain their current values; endTime: null removes the end time. Canonical reference: /docs/reference/client/client#editevent ### Parameters - `eventId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `changes: { description?: string; endTime?: number | null; location?: string; name?: string; startTime?: number }`: ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.editEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", changes: {"description":"Updated meetup details"}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/events/editEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "eventId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "changes": { "description": "Updated meetup details" } }' ``` Aliases: events.editEvent ## editMessage Edits the text of a message through the installed private patch Canonical reference: /docs/reference/client/client#editmessage ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `newContent: string`: Replacement message text ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.editMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", newContent: "newContent", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/messages/edit" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "newContent": "newContent" }' ``` Aliases: messages.edit ## endCall Request hangup for this call. Canonical reference: /docs/reference/client/client#endcall License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.endCall({ call: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/endCall" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {} }' ``` Aliases: calls.endCall ## forwardMessages Forwards messages to a chat Canonical reference: /docs/reference/client/client#forwardmessages ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `messages: string[] | string`: Message ID(s) to process Branded type: MessageId[] - `skipMyMessages?: boolean`: Skip own messages ### Returns `Promise` Resolves to `MessageIdReturn[] \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.forwardMessages({ to: "447123456789@c.us", messages: ["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"], }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/forward" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "messages": [ "false_447123456789@c.us_ABC123", "false_447123456789@c.us_DEF456" ] }' ``` Aliases: messages.forward ## getActiveCall Get the current call snapshot. SDK clients hydrate its bound methods. Canonical reference: /docs/reference/client/client#getactivecall License: restricted ### Parameters ### Returns `Promise<{ id: string; sessionId: string; generation: string; peer: string; state: string; direction: "incoming" | "outgoing"; video: boolean; terminal: boolean; observedAt: number | "Infinity" | "-Infinity" | "NaN"; endReason?: { code: string; message: string; source: "local" | "media" | "license" | "session" | "whatsapp" } | null; media?: { state: "attached" | "ended"; reason?: string | null } | null>` Resolves to `{ id: string; sessionId: string; generation: string; peer: string; state: string; direction: "incoming" \| "outgoing"; video: boolean; terminal: boolean; observedAt: number \| "Infinity" \| "-Infinity" \| "NaN"; endReason?: { code: string; message: string; source: "local" \| "media" \| "license" \| "session" \| "whatsapp" } \| null; media?: { state: "attached" \| "ended"; reason?: string \| null } \| null`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getActiveCall(); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/getActiveCall" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: calls.getActiveCall ## getAlbumMessages Returns currently loaded image and video messages associated with an album, including GIF videos. The album can arrive before its media, so the array may be incomplete. Returns false if the album is not loaded or the ID belongs to another message type. Canonical reference: /docs/reference/client/client#getalbummessages ### Parameters - `messageId: string`: Album container message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` A snapshot of currently loaded album media. An empty or partial array does not mean that the sender sent an empty or partial album. Each item retains its caption and can be passed to decryptMedia or downloadMedia. [Message](/docs/reference/client/return-types#message) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAlbumMessages({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/messages/getAlbum" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Aliases: messages.getAlbum ## getAllChatIds Get all chat IDs Canonical reference: /docs/reference/client/client#getallchatids ### Parameters ### Returns `Promise` Resolves to `ChatId[]`. A rejected Promise indicates input validation or dispatch failure. [ChatId](/docs/reference/client/return-types#chatid) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllChatIds(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/chatIds/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: chatIds.all, chatIds.getAll, chatIds.list, chats.ids, listChatIds ## getAllChats Returns serialized Chat objects from the current session, optionally limited to chats with unread messages. Canonical reference: /docs/reference/client/client#getallchats ### Parameters - `withNewMessagesOnly?: boolean`: Only chats with unread messages ### Returns `Promise` Resolves to `Chat[]`. A rejected Promise indicates input validation or dispatch failure. [Chat](/docs/reference/client/return-types#chat) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllChats(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/chats/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: chats.all, chats.getAll, chats.list, listChats ## getAllCommunities Get all communities Canonical reference: /docs/reference/client/client#getallcommunities ### Parameters ### Returns `Promise` Resolves to `GroupChatId[]`. A rejected Promise indicates input validation or dispatch failure. [GroupChatId](/docs/reference/client/return-types#groupchatid) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllCommunities(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/communities/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: communities.all, communities.getAll, communities.list, listCommunities ## getAllContacts Retrieves all contacts Canonical reference: /docs/reference/client/client#getallcontacts ### Parameters ### Returns `Promise` Resolves to `Contact[]`. A rejected Promise indicates input validation or dispatch failure. [Contact](/docs/reference/client/return-types#contact) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllContacts(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/contacts/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: contacts.all, contacts.getAll, contacts.list, listContacts ## getAllGroups Get all groups Canonical reference: /docs/reference/client/client#getallgroups ### Parameters - `withNewMessagesOnly?: boolean`: Only chats with unread messages ### Returns `Promise` Resolves to `Chat[]`. A rejected Promise indicates input validation or dispatch failure. [Chat](/docs/reference/client/return-types#chat) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllGroups(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/groups/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: groups.all, groups.getAll, groups.list, listGroups ## getAllLabels Get all labels Canonical reference: /docs/reference/client/client#getalllabels ### Parameters ### Returns `Promise` Resolves to `Label[]`. A rejected Promise indicates input validation or dispatch failure. [Label](/docs/reference/client/return-types#label) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllLabels(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/labels/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: labels.all, labels.getAll, labels.list, listLabels ## getAllMessages Retrieves all messages in the session Canonical reference: /docs/reference/client/client#getallmessages ### Parameters - `chatId?: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `includeMe?: boolean`: Include own messages - `includeNotifications?: boolean`: Include notification messages ### Returns `Promise` Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. [Message](/docs/reference/client/return-types#message) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllMessages(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/messages/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: listMessages, messages.all, messages.getAll, messages.list ## getBatteryLevel Get phone battery level Canonical reference: /docs/reference/client/client#getbatterylevel ### Parameters ### Returns `Promise` Resolves to `number`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBatteryLevel(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getBatteryLevel" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getBatteryLevel ## getBlockedIds Get blocked contact IDs Canonical reference: /docs/reference/client/client#getblockedids ### Parameters ### Returns `Promise` Resolves to `ContactId[] \| false`. A rejected Promise indicates input validation or dispatch failure. [ContactId](/docs/reference/client/return-types#contactid) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBlockedIds(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/contacts/getBlockedIds" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: contacts.getBlockedIds ## getBusinessProducts Get business products Canonical reference: /docs/reference/client/client#getbusinessproducts ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `Product[] \| false`. A rejected Promise indicates input validation or dispatch failure. [Product](/docs/reference/client/return-types#product) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBusinessProducts({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/business/getProducts" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: business.getProducts, business.getProfilesProducts, getBusinessProfilesProducts ## getBusinessProfile Get business profile Canonical reference: /docs/reference/client/client#getbusinessprofile ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `BusinessProfile`. A rejected Promise indicates input validation or dispatch failure. [BusinessProfile](/docs/reference/client/return-types#businessprofile) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBusinessProfile({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/business/getProfile" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: business.getProfile ## getCallCapabilities Optional calling availability for UI and diagnostics. Actions perform admission automatically. Canonical reference: /docs/reference/client/client#getcallcapabilities ### Parameters ### Returns `Promise<{ control: boolean; audio: boolean; video: boolean; reason?: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" } }>` Resolves to `{ control: boolean; audio: boolean; video: boolean; reason?: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" } }`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCallCapabilities(); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/getCallCapabilities" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: calls.getCallCapabilities ## getChat Get specific chat by ID Canonical reference: /docs/reference/client/client#getchat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `Chat \| null \| undefined \| false`. A rejected Promise indicates input validation or dispatch failure. [Chat](/docs/reference/client/return-types#chat) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/chats/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Aliases: chats.get, chats.getById, getChatById ## getChatsByLabel Get chats by label Canonical reference: /docs/reference/client/client#getchatsbylabel ### Parameters - `label: string`: Label name ### Returns `Promise` Resolves to `Chat[]`. A rejected Promise indicates input validation or dispatch failure. [Chat](/docs/reference/client/return-types#chat) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getChatsByLabel({ label: "label", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/labels/getChats" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "label=label" ``` Aliases: labels.getChats, labels.getChatsBy ## getChatWithNonContacts Get chats with non-contacts Canonical reference: /docs/reference/client/client#getchatwithnoncontacts ### Parameters ### Returns `Promise` Resolves to `Contact[]`. A rejected Promise indicates input validation or dispatch failure. [Contact](/docs/reference/client/return-types#contact) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getChatWithNonContacts(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/chats/getWithNonContacts" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: chats.getWithNonContacts ## getCommonGroups Get common groups with contact Canonical reference: /docs/reference/client/client#getcommongroups ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `CommonGroup[] \| false`. A rejected Promise indicates input validation or dispatch failure. [GroupId](/docs/reference/client/return-types#groupid), [CommonGroup](/docs/reference/client/return-types#commongroup) In-process Client: `Promise>`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommonGroups({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/contacts/getCommonGroups" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: contacts.getCommonGroups ## getCommunityAdminIds Get community admin IDs Canonical reference: /docs/reference/client/client#getcommunityadminids ### Parameters - `communityId: string`: Community ID A WhatsApp community JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `CommunityAdminIds[]`. A rejected Promise indicates input validation or dispatch failure. [CommunityAdminIds](/docs/reference/client/return-types#communityadminids) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityAdminIds({ communityId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/communities/getAdminIds" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Aliases: communities.getAdminIds ## getCommunityAdmins Get community admins Canonical reference: /docs/reference/client/client#getcommunityadmins ### Parameters - `communityId: string`: Community ID A WhatsApp community JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `CommunityAdmins[]`. A rejected Promise indicates input validation or dispatch failure. [CommunityAdmins](/docs/reference/client/return-types#communityadmins) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityAdmins({ communityId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/communities/getAdmins" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Aliases: communities.getAdmins ## getCommunityInfo Get community metadata Canonical reference: /docs/reference/client/client#getcommunityinfo ### Parameters - `communityId: string`: Community ID A WhatsApp community JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `CommunityMetadata`. A rejected Promise indicates input validation or dispatch failure. [CommunityMetadata](/docs/reference/client/return-types#communitymetadata) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityInfo({ communityId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/communities/getInfo" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Aliases: communities.getInfo ## getCommunityParticipantIds Get community participant IDs Canonical reference: /docs/reference/client/client#getcommunityparticipantids ### Parameters - `communityId: string`: Community ID A WhatsApp community JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `CommunityParticipantIds[]`. A rejected Promise indicates input validation or dispatch failure. [CommunityParticipantIds](/docs/reference/client/return-types#communityparticipantids) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityParticipantIds({ communityId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/communities/getParticipantIds" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Aliases: communities.getParticipantIds ## getCommunityParticipants Get community participants Canonical reference: /docs/reference/client/client#getcommunityparticipants ### Parameters - `communityId: string`: Community ID A WhatsApp community JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `CommunityParticipants[]`. A rejected Promise indicates input validation or dispatch failure. [CommunityParticipants](/docs/reference/client/return-types#communityparticipants) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityParticipants({ communityId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/communities/getParticipants" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Aliases: communities.getParticipants ## getConnectionState Get connection state Canonical reference: /docs/reference/client/client#getconnectionstate ### Parameters ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getConnectionState(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getConnectionState" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getConnectionState ## getContact Get specific contact Canonical reference: /docs/reference/client/client#getcontact ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `Contact \| null`. A rejected Promise indicates input validation or dispatch failure. [Contact](/docs/reference/client/return-types#contact) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getContact({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/contacts/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: contacts.get ## getEvent Retrieve an event by message ID from WhatsApp local history. Returns null when absent. Canonical reference: /docs/reference/client/client#getevent ### Parameters - `eventId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `WhatsAppEvent \| null`. A rejected Promise indicates input validation or dispatch failure. [WhatsAppEvent](/docs/reference/client/return-types#whatsappevent) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/events/getEvent" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "eventId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Aliases: events.getEvent ## getEventResponses Get the latest decrypted RSVP per participant, including responses persisted in WhatsApp local history. Canonical reference: /docs/reference/client/client#geteventresponses ### Parameters - `eventId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `EventResponse[]`. A rejected Promise indicates input validation or dispatch failure. [EventResponse](/docs/reference/client/return-types#eventresponse) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getEventResponses({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/events/getEventResponses" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "eventId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Aliases: events.getEventResponses ## getEvents Page through a chat’s locally synced event history, newest first. Pass nextCursor as options.before to retrieve the next page. Canonical reference: /docs/reference/client/client#getevents ### Parameters - `chatId: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `options?: { before?: string; limit?: number }`: ### Returns `Promise` Resolves to `EventPage`. A rejected Promise indicates input validation or dispatch failure. [EventPage](/docs/reference/client/return-types#eventpage) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getEvents({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/events/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Aliases: events.get ## getFeatures Get enabled features Canonical reference: /docs/reference/client/client#getfeatures ### Parameters ### Returns `Promise` WhatsApp feature flags keyed by name. Values are provider-defined and must be narrowed before use. [FeatureFlags](/docs/reference/client/return-types#featureflags) In-process Client: `Promise>`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getFeatures(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getFeatures" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getFeatures ## getGroupAdmins Get group admins Canonical reference: /docs/reference/client/client#getgroupadmins ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `ContactId[]`. A rejected Promise indicates input validation or dispatch failure. [ContactId](/docs/reference/client/return-types#contactid) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupAdmins({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/groups/getAdmins" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Aliases: groups.getAdmins ## getGroupInfo Get the group title and description through the installed private patch Canonical reference: /docs/reference/client/client#getgroupinfo ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise<{ title: string; description?: string | null } | string | null>` Resolves to `{ title: string; description?: string \| null } \| string \| null`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupInfo({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/groups/getInfo" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Aliases: groups.getInfo ## getGroupInviteLink Get group invite link Canonical reference: /docs/reference/client/client#getgroupinvitelink ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupInviteLink({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/groups/getInviteLink" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Aliases: groups.getInviteLink ## getGroupMembers Retrieves members of a group Canonical reference: /docs/reference/client/client#getgroupmembers ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `Contact[]`. A rejected Promise indicates input validation or dispatch failure. [Contact](/docs/reference/client/return-types#contact) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupMembers({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/groups/getMembers" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Aliases: groups.getMembers ## getGroupMembersId Get group member IDs Canonical reference: /docs/reference/client/client#getgroupmembersid ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `ContactId[]`. A rejected Promise indicates input validation or dispatch failure. [ContactId](/docs/reference/client/return-types#contactid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupMembersId({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/groups/getMemberIds" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Aliases: getGroupMemberIds, groups.getMemberIds, groups.getMembersId ## getHostNumber Get host phone number Canonical reference: /docs/reference/client/client#gethostnumber ### Parameters ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getHostNumber(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getHostNumber" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getHostNumber ## getKickedGroups Get groups where kicked Canonical reference: /docs/reference/client/client#getkickedgroups ### Parameters ### Returns `Promise` Resolves to `GroupChatId[]`. A rejected Promise indicates input validation or dispatch failure. [GroupChatId](/docs/reference/client/return-types#groupchatid) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getKickedGroups(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/groups/getKicked" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: groups.getKicked ## getLicenseFeatures Get enabled and disabled licensed features from the accepted licence patch. This reports licence access; actions check runtime availability automatically. Canonical reference: /docs/reference/client/client#getlicensefeatures ### Parameters ### Returns `Promise<{ status: "valid" | "missing" | "invalid" | "expired" | "metadata_only"; keyType: string | null; expiresAt: number | "Infinity" | "-Infinity" | "NaN" | null; features: Record }>` Resolves to `{ status: "valid" \| "missing" \| "invalid" \| "expired" \| "metadata_only"; keyType: string \| null; expiresAt: number \| "Infinity" \| "-Infinity" \| "NaN" \| null; features: Record }`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getLicenseFeatures(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getLicenseFeatures" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getLicenseFeatures ## getLicenseType Get license type Canonical reference: /docs/reference/client/client#getlicensetype ### Parameters ### Returns `Promise` Resolves to `LicenseType \| false`. A rejected Promise indicates input validation or dispatch failure. [LicenseType](/docs/reference/client/return-types#licensetype) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getLicenseType(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getLicenseType" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getLicenseType ## getLoadedMessageCount Get loaded message count Canonical reference: /docs/reference/client/client#getloadedmessagecount ### Parameters ### Returns `Promise` Resolves to `number`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getLoadedMessageCount(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getLoadedMessageCount" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: getAmountOfLoadedMessages, session.getAmountOfLoadedMessages, session.getLoadedMessageCount ## getMe Get host account info Canonical reference: /docs/reference/client/client#getme ### Parameters ### Returns `Promise` Host account attributes plus the current account ID. Additional WhatsApp Web attributes vary by version. [HostAccount](/docs/reference/client/return-types#hostaccount) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMe(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getMe" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getMe ## getMessageById Retrieves a specific message by ID Canonical reference: /docs/reference/client/client#getmessagebyid ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `Message \| null \| false`. A rejected Promise indicates input validation or dispatch failure. [Message](/docs/reference/client/return-types#message) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMessageById({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/messages/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Aliases: getMessage, messages.get, messages.getById ## getMessageInfo Get message delivery info Canonical reference: /docs/reference/client/client#getmessageinfo ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `MessageInfo`. A rejected Promise indicates input validation or dispatch failure. [MessageInfo](/docs/reference/client/return-types#messageinfo) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMessageInfo({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/messages/getInfo" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Aliases: messages.getInfo ## getMessagesForLLM Get messages formatted for LLMs Canonical reference: /docs/reference/client/client#getmessagesforllm ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `last?: number`: ### Returns `Promise` Resolves to `LLMMessage[]`. A rejected Promise indicates input validation or dispatch failure. [LLMMessage](/docs/reference/client/return-types#llmmessage) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMessagesForLLM({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/messages/getForLLM" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Aliases: getGptArray, messages.getForLLM, messages.getGptArray ## getMyLastMessage Get last message sent by host Canonical reference: /docs/reference/client/client#getmylastmessage ### Parameters - `chatId?: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `Message \| undefined`. A rejected Promise indicates input validation or dispatch failure. [Message](/docs/reference/client/return-types#message) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMyLastMessage(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/messages/getMyLast" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: messages.getMyLast ## getNumberProfile Get profile of number Canonical reference: /docs/reference/client/client#getnumberprofile License: insiders ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Provisional number profile based on the public Contact fields. Fields are optional and additional provider fields are unknown. The licensed getNumberProfile payload has not been verified against this model. [NumberProfile](/docs/reference/client/return-types#numberprofile) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getNumberProfile({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/contacts/getNumberProfile" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: contacts.getNumberProfile ## getOrder Get order details Canonical reference: /docs/reference/client/client#getorder License: insiders ### Parameters - `id: string`: Order or message ID ### Returns `Promise` Resolves to `Order`. A rejected Promise indicates input validation or dispatch failure. [Order](/docs/reference/client/return-types#order) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getOrder({ id: "id", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/business/getOrder" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "id=id" ``` Aliases: business.getOrder ## getPollResults Hydrates the local poll vote store and returns synced counts, choices and votes. Hidden-voter polls omit voter identities and the votes array. Unsynced votes may be absent. Canonical reference: /docs/reference/client/client#getpollresults ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `PollResults`. A rejected Promise indicates input validation or dispatch failure. [PollResults](/docs/reference/client/return-types#pollresults) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getPollResults({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/getPollResults" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: messages.getPollResults ## getProcessStats Get process statistics Canonical reference: /docs/reference/client/client#getprocessstats ### Parameters ### Returns `Promise` Process diagnostics keyed by process ID. The provider controls each entry; no stable entry shape is guaranteed. [ProcessStats](/docs/reference/client/return-types#processstats) In-process Client: `Promise>`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getProcessStats(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getProcessStats" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getProcessStats ## getProfilePicture Get profile picture from server Canonical reference: /docs/reference/client/client#getprofilepicture ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getProfilePicture({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/contacts/getProfilePicture" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Aliases: contacts.getProfilePicFromServer, contacts.getProfilePicture, getProfilePicFromServer ## getSnapshot Take screenshot Canonical reference: /docs/reference/client/client#getsnapshot ### Parameters - `chatId?: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `width?: number`: - `height?: number`: ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getSnapshot(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getSnapshot" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.getSnapshot, session.takeScreenshot, takeScreenshot ## getStarredMessages Get starred messages Canonical reference: /docs/reference/client/client#getstarredmessages License: insiders ### Parameters - `chatId?: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. [Message](/docs/reference/client/return-types#message) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getStarredMessages(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/messages/getStarred" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: messages.getStarred ## getStatus Get specific status Canonical reference: /docs/reference/client/client#getstatus License: insiders ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `ContactStatus`. A rejected Promise indicates input validation or dispatch failure. [ContactStatus](/docs/reference/client/return-types#contactstatus) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getStatus({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/mystatus/getStatus" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Aliases: mystatus.getStatus ## getStories Get all stories Canonical reference: /docs/reference/client/client#getstories License: insiders ### Parameters ### Returns `Promise` Resolves to `Story[]`. A rejected Promise indicates input validation or dispatch failure. [Story](/docs/reference/client/return-types#story) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getStories(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/status/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: getStatuses, status.get, status.getAll, status.getStories ## getUnsentMessages Get unsent/pending messages Canonical reference: /docs/reference/client/client#getunsentmessages ### Parameters ### Returns `Promise` Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. [Message](/docs/reference/client/return-types#message) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getUnsentMessages(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/messages/getUnsent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: messages.getUnsent ## getUserAgent Get user agent Canonical reference: /docs/reference/client/client#getuseragent ### Parameters - `userAgent?: string`: User agent string ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getUserAgent(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getUserAgent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: getGeneratedUserAgent, session.getGeneratedUserAgent, session.getUserAgent ## getVCards Extract vCards from message Canonical reference: /docs/reference/client/client#getvcards ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `string[]`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getVCards({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/messages/getVCards" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Aliases: messages.getVCards ## getWAVersion Get WhatsApp Web version Canonical reference: /docs/reference/client/client#getwaversion ### Parameters ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getWAVersion(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/getWAVersion" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: getWhatsAppVersion, session.getWAVersion, session.getWhatsAppVersion ## healthCheck Health check Canonical reference: /docs/reference/client/client#healthcheck ### Parameters ### Returns `Promise` Browser health diagnostics. This is not the Easy API /health HTTP envelope. [HealthCheck](/docs/reference/client/return-types#healthcheck) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.healthCheck(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/healthCheck" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: session.healthCheck ## isChatOnline Check if chat is online Canonical reference: /docs/reference/client/client#ischatonline ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.isChatOnline({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -G "http://localhost:8080/api/chats/isOnline" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Aliases: chats.isOnline ## isPlugged Check if phone is charging Canonical reference: /docs/reference/client/client#isplugged ### Parameters ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.isPlugged(); ``` ### HTTP ```bash curl -X GET "http://localhost:8080/api/session/isPlugged" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Aliases: getIsPlugged, session.getIsPlugged, session.isPlugged ## joinGroupViaLink Join group via invite link Canonical reference: /docs/reference/client/client#joingroupvialink License: restricted ### Parameters - `link: string`: Group invite link - `returnChatObj?: boolean`: Return chat object ### Returns `Promise` Resolves to `GroupChatId \| boolean \| number \| Chat`. A rejected Promise indicates input validation or dispatch failure. [Chat](/docs/reference/client/return-types#chat), [GroupChatId](/docs/reference/client/return-types#groupchatid), [GroupId](/docs/reference/client/return-types#groupid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.joinGroupViaLink({ link: "link", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/groups/join" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "link": "link" }' ``` Aliases: groups.join, groups.joinViaLink, joinGroup ## leaveGroup Leave a group Canonical reference: /docs/reference/client/client#leavegroup ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `void`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.leaveGroup({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/leave" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us" }' ``` Aliases: groups.leave ## loadEarlierMessages Loads earlier messages from a chat Canonical reference: /docs/reference/client/client#loadearliermessages ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `count?: number`: Number of messages to load (default: 20) - `includeMe?: boolean`: Whether to include messages from the host account (default: false) ### Returns `Promise` Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. [Message](/docs/reference/client/return-types#message) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.loadEarlierMessages({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/loadEarlier" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: loadOlderMessages, messages.loadEarlier, messages.loadOlder ## markAsRead Mark chat as read Canonical reference: /docs/reference/client/client#markasread ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.markAsRead({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/markAsRead" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.markAsRead ## markAsUnread Mark chat as unread Canonical reference: /docs/reference/client/client#markasunread ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.markAsUnread({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/markAsUnread" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.markAsUnread ## muteCall Mute or unmute the microphone without ending reception. Canonical reference: /docs/reference/client/client#mutecall License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: - `muted: boolean`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.muteCall({ call: {}, muted: true, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/muteCall" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {}, "muted": true }' ``` Aliases: calls.muteCall ## muteChat Mute a chat Canonical reference: /docs/reference/client/client#mutechat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `muteDuration: number | string`: Mute duration ### Returns `Promise` Resolves to `string \| boolean \| number`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.muteChat({ chatId: "447123456789@c.us", muteDuration: 1, }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/mute" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us", "muteDuration": 1 }' ``` Aliases: chats.mute ## observeCallAudio Attach an isolated receive-only audio sink. Its failure does not interrupt the call owner. Canonical reference: /docs/reference/client/client#observecallaudio License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: - `speaker?: unknown`: ### Returns `Promise<{ ok: true; status: "requested"; observerId: string } | { ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; observerId: string } \| { ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.observeCallAudio({ call: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/observeCallAudio" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {} }' ``` Aliases: calls.observeCallAudio ## pinChat Pin a chat Canonical reference: /docs/reference/client/client#pinchat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.pinChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/pin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.pin ## pinMessage Pins a message in its chat for everyone, subject to native permissions. This differs from pinChat, which changes the current account chat list. Canonical reference: /docs/reference/client/client#pinmessage ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `duration?: "OneDay" | "SevenDays" | "ThirtyDays"`: ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.pinMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/pin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: messages.pin ## postImageStatus Post image status Canonical reference: /docs/reference/client/client#postimagestatus License: insiders ### Parameters - `image: string`: Image data URL - `caption?: string`: Status caption ### Returns `Promise` Resolves to `MessageId \| string \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.postImageStatus({ image: "image", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/status/postImage" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "image": "image" }' ``` Aliases: status.postImage ## postTextStatus Post text status Canonical reference: /docs/reference/client/client#posttextstatus License: insiders ### Parameters - `text: string`: Status text - `backgroundColor?: string`: Background color - `font?: number`: Font style ### Returns `Promise` Resolves to `MessageId \| string \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.postTextStatus({ text: "text", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/status/postText" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "text": "text" }' ``` Aliases: status.postText ## postVideoStatus Post video status Canonical reference: /docs/reference/client/client#postvideostatus License: insiders ### Parameters - `video: string`: Video data URL - `caption?: string`: Status caption ### Returns `Promise` Resolves to `MessageId \| string \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.postVideoStatus({ video: "video", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/status/postVideo" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "video": "video" }' ``` Aliases: status.postVideo ## promoteParticipant Promote participant to admin Canonical reference: /docs/reference/client/client#promoteparticipant ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.promoteParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/promoteParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Aliases: groups.promoteParticipant, promoteGroupParticipant ## react React to message with emoji Canonical reference: /docs/reference/client/client#react ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `emoji: string`: Emoji reaction ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.react({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", emoji: "emoji", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/react" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "emoji": "emoji" }' ``` Aliases: messages.react ## rejectCall Reject this call with native state confirmation. Canonical reference: /docs/reference/client/client#rejectcall License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.rejectCall({ call: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/rejectCall" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {} }' ``` Aliases: calls.rejectCall ## rejectGroupJoinRequest Reject group join request Canonical reference: /docs/reference/client/client#rejectgroupjoinrequest ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `string \| boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.rejectGroupJoinRequest({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/rejectJoinRequest" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Aliases: groups.rejectJoinRequest ## removeLabel Remove label from chat Canonical reference: /docs/reference/client/client#removelabel ### Parameters - `label: string`: Label ID or name - `chatId: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.removeLabel({ label: "label", chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/labels/remove" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "label": "label", "chatId": "447123456789@c.us" }' ``` Aliases: labels.remove ## removeParticipant Remove participant from group Canonical reference: /docs/reference/client/client#removeparticipant ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.removeParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/removeParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Aliases: groups.removeParticipant, removeGroupParticipant ## requestPhoneNumber Ask a direct LID chat to voluntarily share their phone number. A submitted request does not confirm sharing. Canonical reference: /docs/reference/client/client#requestphonenumber ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Message ID [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.requestPhoneNumber({ to: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/contacts/requestPhoneNumber" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us" }' ``` Aliases: contacts.requestPhoneNumber ## respondToEvent RSVP through WhatsApp’s native encrypted event-response action. Canonical reference: /docs/reference/client/client#respondtoevent ### Parameters - `eventId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `response: "going" | "notGoing" | "maybe"`: ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.respondToEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", response: "going", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/events/respondToEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "eventId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "response": "going" }' ``` Aliases: events.respondToEvent ## revokeGroupInviteLink Revoke group invite link Canonical reference: /docs/reference/client/client#revokegroupinvitelink ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ ### Returns `Promise` Resolves to `string \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.revokeGroupInviteLink({ groupId: "447123456789-1445627445@g.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/revokeInviteLink" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us" }' ``` Aliases: groups.revokeInviteLink ## sendAdvancedButtons Sends an advanced button message with media (deprecated) Canonical reference: /docs/reference/client/client#sendadvancedbuttons License: insiders ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `body?: string | any`: Message body, location, or media - `buttons: any[]`: Array of advanced buttons (max 3) - `text: string`: Message text - `footer: string`: Message footer - `filename: string`: Filename if body is a file ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendAdvancedButtons({ to: "447123456789@c.us", buttons: [], text: "text", footer: "footer", filename: "filename", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendAdvancedButtons" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "buttons": [], "text": "text", "footer": "footer", "filename": "filename" }' ``` Aliases: messages.sendAdvancedButtons ## sendAlbum Sends images and videos together as a native WhatsApp album with one caption on its first item. GIFs use MP4 video data with isGif: true. Documents are rejected. Phone-number IDs are resolved to LIDs; the destination chat must already be loaded and allow sending. WhatsApp controls the minimum item count and upload limits. Runtime failures include the parent and submitted child IDs when available; inspect the chat before retrying. Canonical reference: /docs/reference/client/client#sendalbum ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `media: { file: string; filename: string; isGif?: boolean }[]`: Album items in send order - `caption?: string`: Media caption - `quotedMsgId?: string`: Message to quote on the first album item A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Submitted album parent and child message IDs, in input order. Submission does not confirm recipient delivery. Runtime failures include the album ID and submitted child IDs when available. A lost connection can leave the send outcome unknown; inspect the chat before retrying. [AlbumSendResult](/docs/reference/client/return-types#albumsendresult) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); import { readFile } from 'node:fs/promises'; // Put these real image files in your process working directory. const media = await Promise.all(['photo-1.jpg', 'photo-2.jpg', 'photo-3.jpg', 'photo-4.jpg'].map(async filename => ({ filename, file: `data:image/jpeg;base64,${(await readFile(filename)).toString('base64')}`, }))); const result = await client.sendAlbum({ to: '447123456789@c.us', media, caption: 'One caption for the album', }); ``` ### HTTP ```bash # Save an album request as album.json with "to", "caption", and "media". # Each media item needs "filename" and "file" containing a real image/video data URL. # See /docs/guides/messages#albums for encoding real files. curl -X POST "http://localhost:8080/api/messages/sendAlbum" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data-binary @album.json ``` Aliases: messages.sendAlbum ## sendAudio Sends an audio attachment with an optional quoted message. Use sendPtt for voice notes and view-once audio. Canonical reference: /docs/reference/client/client#sendaudio ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `file: string | Buffer`: Audio file (path, URL, DataURL, or Base64) Branded type: DataURL - `filename?: string`: Filename with extension - `options?: { quotedMsgId?: string }`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendAudio({ to: "447123456789@c.us", file: "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA...", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendAudio" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..." }' ``` Aliases: messages.sendAudio ## sendBanner Sends a banner image Canonical reference: /docs/reference/client/client#sendbanner ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `base64: string`: Base64 encoded JPEG ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendBanner({ to: "447123456789@c.us", base64: "base64", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendBanner" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "base64": "base64" }' ``` Aliases: messages.sendBanner ## sendButtons Sends a button message (deprecated) Canonical reference: /docs/reference/client/client#sendbuttons License: insiders ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `body?: string | any`: Message body or location - `buttons: any[]`: Array of buttons (max 3) - `title?: string`: Message title - `footer?: string`: Message footer ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendButtons({ to: "447123456789@c.us", buttons: [], }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendButtons" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "buttons": [] }' ``` Aliases: messages.sendButtons ## sendContact Sends a single contact Canonical reference: /docs/reference/client/client#sendcontact ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `contactId: string | string[]`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean \| (MessageIdReturn \| boolean)[]`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn), [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendContact({ to: "447123456789@c.us", contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendContact" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "contactId": "447123456789@c.us" }' ``` Aliases: messages.sendContact ## sendEvent Send a native WhatsApp event with Unix-second times. Returns its submitted message ID; delivery is observed through ACKs. Canonical reference: /docs/reference/client/client#sendevent ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `event: { callType?: "voice" | "video"; description?: string; endTime?: number; extraGuestsAllowed?: boolean; isScheduledCall?: boolean; location?: string; name: string; quotedMsgId?: string; requireApproval?: boolean; startTime: number }`: ### Returns `Promise` Message ID [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendEvent({ to: "447123456789@c.us", event: {"name":"Community meetup","startTime":1893456000,"description":"Meet the community"}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/events/sendEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "event": { "name": "Community meetup", "startTime": 1893456000, "description": "Meet the community" } }' ``` Aliases: events.sendEvent ## sendFile Sends an image, video, audio file or document using MIME detection or an explicit typed presentation. Incompatible options are rejected before sending. Media is not transcoded. Canonical reference: /docs/reference/client/client#sendfile ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `file: string | Buffer`: File (path, URL, DataURL, Base64, or Buffer) Branded type: DataURL - `filename?: string`: Filename with extension - `options?: { caption?: string; quotedMsgId?: string; type?: "auto"; viewOnce?: boolean } | { caption?: string; quotedMsgId?: string; type: "image"; viewOnce?: boolean } | { caption?: string; isGif?: boolean; quotedMsgId?: string; type: "video"; videoNote?: boolean; viewOnce?: boolean } | { ptt?: boolean; quotedMsgId?: string; type: "audio"; viewOnce?: boolean } | { caption?: string; quotedMsgId?: string; type: "document" }`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendFile({ to: "447123456789@c.us", file: "data:application/pdf;base64,JVBERi0xLjQK...", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendFile" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:application/pdf;base64,JVBERi0xLjQK..." }' ``` Aliases: messages.sendFile ## sendFileFromUrl Downloads media and sends it through sendFile with the same typed presentation options. Canonical reference: /docs/reference/client/client#sendfilefromurl ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `url: string`: URL of the file to download - `filename?: string`: Filename with extension - `options?: { caption?: string; quotedMsgId?: string; type?: "auto"; viewOnce?: boolean } | { caption?: string; quotedMsgId?: string; type: "image"; viewOnce?: boolean } | { caption?: string; isGif?: boolean; quotedMsgId?: string; type: "video"; videoNote?: boolean; viewOnce?: boolean } | { ptt?: boolean; quotedMsgId?: string; type: "audio"; viewOnce?: boolean } | { caption?: string; quotedMsgId?: string; type: "document" }`: - `headers?: object`: HTTP headers for the download request ### Returns `Promise` Message ID or success status [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendFileFromUrl({ to: "447123456789@c.us", url: "url", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendFileFromUrl" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "url": "url" }' ``` Aliases: messages.sendFileFromUrl ## sendImage Sends media through the native attachment pipeline, including images and videos. Options select presentation, view once, GIF playback or circular video notes; media is not transcoded. Returns submission status, not a delivery receipt. Canonical reference: /docs/reference/client/client#sendimage ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `file: string | Buffer`: File (path, URL, DataURL, Base64, or Buffer) Branded type: DataURL - `filename?: string`: Filename with extension - `options?: { caption?: string; quotedMsgId?: string; type?: "auto"; viewOnce?: boolean } | { caption?: string; quotedMsgId?: string; type: "image"; viewOnce?: boolean } | { caption?: string; isGif?: boolean; quotedMsgId?: string; type: "video"; videoNote?: boolean; viewOnce?: boolean } | { ptt?: boolean; quotedMsgId?: string; type: "audio"; viewOnce?: boolean } | { caption?: string; quotedMsgId?: string; type: "document" }`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendImage({ to: "447123456789@c.us", file: "data:application/pdf;base64,JVBERi0xLjQK...", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendImage" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:application/pdf;base64,JVBERi0xLjQK..." }' ``` Aliases: messages.sendImage ## sendInteractive Send a portable interactive message document (Insiders and above) Canonical reference: /docs/reference/client/client#sendinteractive License: insiders ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `content: { actions: ({ id: string; label: string; type: "reply" } | { label: string; type: "url"; url: string } | { label: string; phoneNumber: string; type: "call" } | { code: string; label: string; type: "copy" })[]; body: string; footer?: string; header?: { text: string; type: "text" } | { filename?: string; source: string; title?: string; type: "image" } | { filename?: string; source: string; title?: string; type: "video" } | { filename?: string; source: string; title?: string; type: "document" }; type: "buttons"; version?: 1 } | { body: string; buttonLabel?: string; footer?: string; sections: { rows: { description?: string; id: string; title: string }[]; title?: string }[]; title?: string; type: "list"; version?: 1 } | { body: string; footer?: string; questions: ({ allowCustomAnswer?: boolean; id: string; label: string; options: { id: string; label: string }[]; type: "singleSelect" | "multiSelect" })[]; title?: string; type: "form"; version?: 1 } | { body: string; cards: ({ actions: ({ id: string; label: string; type: "reply" } | { label: string; type: "url"; url: string } | { label: string; phoneNumber: string; type: "call" } | { code: string; label: string; type: "copy" })[]; body: string; header: { filename?: string; source: string; title?: string; type: "image" } })[]; footer?: string; type: "carousel"; version?: 1 } | { actions?: ({ label: string; type: "url"; url: string } | { label: string; phoneNumber: string; type: "call" } | { code: string; label: string; type: "copy" })[]; body: string; booking: { description?: string; email?: string; endAt?: string; labels?: { addToCalendar?: string; detailsTitle?: string; language?: string; manageBooking?: string; meetingType?: string; viewOnMap?: string }; location?: string; managementUrl?: string; phoneNumber?: string; startAt: string; url?: string }; buttonLabel?: string; footer?: string; title?: string; type: "booking"; version?: 1 } | { actions: { name: string; parameters?: object }[]; body: string; footer?: string; header?: { text: string; type: "text" } | { filename?: string; source: string; title?: string; type: "image" } | { filename?: string; source: string; title?: string; type: "video" } | { filename?: string; source: string; title?: string; type: "document" }; parameters?: object; type: "native"; version?: 1 }`: ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendInteractive({ to: "447123456789@c.us", content: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendInteractive" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": {} }' ``` Aliases: messages.sendInteractive ## sendLinkWithAutoPreview Sends a link with auto-generated preview Canonical reference: /docs/reference/client/client#sendlinkwithautopreview ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `url: string`: Link URL - `text?: string`: Message content Branded type: Content - `thumbnail?: string`: Base64 thumbnail override - `quotedMsgId?: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `customSize?: { height: number; width: number }`: Custom thumbnail size ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendLinkWithAutoPreview({ to: "447123456789@c.us", url: "url", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendLinkWithAutoPreview" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "url": "url" }' ``` Aliases: messages.sendLink, messages.sendLinkWithAutoPreview, sendLink ## sendListMessage Sends a list/menu message (deprecated) Canonical reference: /docs/reference/client/client#sendlistmessage License: insiders ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `sections: any[]`: List sections - `title: string`: List title - `description: string`: List description - `actionText: string`: Action button text ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendListMessage({ to: "447123456789@c.us", sections: [], title: "title", description: "description", actionText: "actionText", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendList" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "sections": [], "title": "title", "description": "description", "actionText": "actionText" }' ``` Aliases: messages.sendList, sendList ## sendLocation Sends a location to a chat Canonical reference: /docs/reference/client/client#sendlocation ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `lat: number | string`: Latitude coordinate - `lng: number | string`: Longitude coordinate - `loc?: string`: Location name or description - `address?: string`: Address text - `url?: string`: Address link ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn), [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendLocation({ to: "447123456789@c.us", lat: 51.5074, lng: -0.1278, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendLocation" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "lat": 51.5074, "lng": -0.1278 }' ``` Aliases: messages.sendLocation ## sendMessageWithThumb Sends a link preview message with custom thumbnail Canonical reference: /docs/reference/client/client#sendmessagewiththumb ### Parameters - `thumb: string`: Base64 thumbnail (max 200x200px) - `url: string`: Link URL - `title: string`: Link title - `description: string`: Link description - `content: string`: Message content Branded type: Content - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `quotedMsgId?: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `customSize?: { height: number; width: number }`: Custom thumbnail size ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendMessageWithThumb({ thumb: "thumb", url: "url", title: "title", description: "description", content: "Hello, world!", to: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendLinkWithThumbnail" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "thumb": "thumb", "url": "url", "title": "title", "description": "description", "content": "Hello, world!", "to": "447123456789@c.us" }' ``` Aliases: messages.sendLinkWithThumbnail, messages.sendWithThumb, sendLinkWithThumbnail ## sendMultipleContacts Sends multiple contacts Canonical reference: /docs/reference/client/client#sendmultiplecontacts ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `contactIds: string[]`: A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn), [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendMultipleContacts({ to: "447123456789@c.us", contactIds: ["447123456789@c.us"], }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendContacts" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "contactIds": [ "447123456789@c.us" ] }' ``` Aliases: messages.sendContacts, messages.sendMultipleContacts, sendContacts ## sendPaymentRequest Sends a payment request message Canonical reference: /docs/reference/client/client#sendpaymentrequest ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `amount: number`: Amount in 1000 format (e.g £10 => 10000) - `currency: string`: 3 letter currency code - `message?: string`: Optional message ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendPaymentRequest({ to: "447123456789@c.us", amount: 1, currency: "currency", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendPaymentRequest" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "amount": 1, "currency": "currency" }' ``` Aliases: messages.sendPaymentRequest ## sendPoll Sends a text poll with a selection limit, quote, optional closing time in Unix milliseconds and hidden voter names. Closing time and hidden names require native WhatsApp support for the sending account/chat. These fields select the native V6 poll payload. Canonical reference: /docs/reference/client/client#sendpoll ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `name: string`: Poll question - `options: string[]`: Poll choices; the account may have a lower native limit - `config?: { endTime?: number; hideVoterNames?: boolean; quotedMsgId?: string; selectableCount?: number }`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn), [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendPoll({ to: "447123456789@c.us", name: "name", options: ["Yes", "No"], }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendPoll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "name": "name", "options": [ "Yes", "No" ] }' ``` Aliases: messages.sendPoll ## sendPtt Sends prepared audio as a voice note, optionally view once. It does not transcode the input; use suitable voice-note audio such as Ogg Opus. Canonical reference: /docs/reference/client/client#sendptt ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `file: string | Buffer`: Audio file (path, URL, DataURL, or Base64) Branded type: DataURL - `options?: { quotedMsgId?: string; viewOnce?: boolean }`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendPtt({ to: "447123456789@c.us", file: "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA...", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendPtt" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..." }' ``` Aliases: messages.sendPtt, messages.sendVoiceNote, sendVoiceNote ## sendRawMessage Send a constructed WhatsApp protobuf Message payload (Insiders and above); media must already be uploaded Canonical reference: /docs/reference/client/client#sendrawmessage License: insiders ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `payload: object`: ### Returns `Promise` Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendRawMessage({ to: "447123456789@c.us", payload: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendRaw" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "payload": {} }' ``` Aliases: messages.sendRaw ## sendReplyWithMentions Sends a reply to a message that includes @mentions Canonical reference: /docs/reference/client/client#sendreplywithmentions ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `content: string`: Message content Branded type: Content - `replyMessageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `hideTags?: boolean`: Removes all tags within the message - `mentions?: string[]`: List of mentioned contact IDs ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendReplyWithMentions({ to: "447123456789@c.us", content: "Hello, world!", replyMessageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendReplyWithMentions" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": "Hello, world!", "replyMessageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: messages.sendReplyWithMentions ## sendSeen Mark message as seen Canonical reference: /docs/reference/client/client#sendseen ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendSeen({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/messages/sendSeen" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: messages.sendSeen ## sendSticker Render a sticker locally using automatic whole-job backend selection and ordered effects, then submit through the private donor finalizer. Author and pack labels are configurable; donor app links are owned by the installed patch. Returns a submitted message ID, not a delivery receipt. Unknown send outcomes must be reconciled before retrying. Canonical reference: /docs/reference/client/client#sendsticker ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `input: string`: A media data URL or HTTP(S) URL. The local library additionally accepts bytes, Blob and local filenames. Pattern: ^(?:data:(?:image|video)\/[^;,]+;base64,[A-Za-z0-9+/]+={0,2}|https?:\/\/\S+)$ - `job?: { author?: string; backend?: "auto" | "browser" | "standalone"; background?: string; circle?: boolean; effects?: (string | { name: string; options?: object })[]; fit?: "contain" | "cover"; fps?: number; loopCount?: number; pack?: string; quality?: number; quotedMsgId?: string; trim?: number[] }`: ### Returns `Promise` Validated serialized message ID. This confirms the send returned an ID, not delivery or reading. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendSticker({ to: "447123456789@c.us", input: "input", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendSticker" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "input": "input" }' ``` Aliases: messages.sendSticker ## sendText Sends a text message and returns a validated serialized message ID. The in-process Client throws SendTextError on failure. An unknown outcome means the message may have been sent: check the chat before retrying. Starting a chat with a new number requires an applied restricted or premium license. Canonical reference: /docs/reference/client/client#sendtext ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `content: string`: Message content Branded type: Content - `options?: unknown`: Reserved for compatibility with legacy request shapes. The current sendText Client implementation ignores this argument; use reply() or sendTextWithMentions() for quoted replies and mentions. Accepted as unknown for compatibility; currently ignored by sendText. Use reply() or sendTextWithMentions() for supported reply and mention behavior. ### Returns `Promise` Resolves to a validated serialized `MessageId` string, not a delivery or read receipt. The in-process Client throws `SendTextError`, exported by `@open-wa/wa-automate` and `@open-wa/client`, on failure. Its `code` is `INVALID_ARGUMENT`, `LICENSE_REQUIRED`, `SEND_REJECTED`, `INVALID_SEND_RESULT`, or `SEND_FAILED`. Its `outcome` is `not_sent` for invalid arguments and known runtime rejections, or `unknown` when the runtime returns no valid ID or browser evaluation fails. An unknown outcome means the message may already have been sent: check the chat before retrying. The client never retries automatically. Starting a chat with a new number still requires an applied restricted or premium license. HTTP and remote clients expose failures through their own error envelopes. [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const messageId = await client.sendText("447123456789@c.us", "Hello, world!"); console.log('Message ID:', messageId); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendText" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": "Hello, world!" }' ``` Aliases: messages.sendText ## sendTextWithMentions Sends a text message that includes @mentions Canonical reference: /docs/reference/client/client#sendtextwithmentions ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `content: string`: Message content Branded type: Content - `hideTags?: boolean`: Removes all tags within the message - `mentions?: string[]`: List of mentioned contact IDs ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendTextWithMentions({ to: "447123456789@c.us", content: "Hello, world!", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendTextWithMentions" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": "Hello, world!" }' ``` Aliases: messages.sendTextWithMentions ## sendVCard Sends a contact card (vCard) Canonical reference: /docs/reference/client/client#sendvcard ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `vcard: string`: vCard string - `contactName?: string`: Contact display name - `contactNumber?: string`: Contact number with country code ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn), [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendVCard({ to: "447123456789@c.us", vcard: "vcard", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendVCard" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "vcard": "vcard" }' ``` Aliases: messages.sendVCard ## sendVideoAsGif Sends MP4 video with WhatsApp GIF playback. GIF is a video presentation flag; this method does not convert a GIF image or transcode video. Canonical reference: /docs/reference/client/client#sendvideoasgif ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `file: string | Buffer`: Video file (path, URL, DataURL, or Base64) Branded type: DataURL - `filename?: string`: Filename with extension - `options?: { caption?: string; quotedMsgId?: string }`: ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendVideoAsGif({ to: "447123456789@c.us", file: "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21...", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendVideoAsGif" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21..." }' ``` Aliases: messages.sendVideoAsGif ## sendYouTubeLink Sends a YouTube link with auto-generated preview Canonical reference: /docs/reference/client/client#sendyoutubelink ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `url: string`: YouTube URL - `text?: string`: Message content Branded type: Content - `thumbnail?: string`: Base64 thumbnail override - `quotedMsgId?: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `customSize?: { height: number; width: number }`: Custom thumbnail size ### Returns `Promise` Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. [MessageIdReturn](/docs/reference/client/return-types#messageidreturn) Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendYouTubeLink({ to: "447123456789@c.us", url: "url", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/sendYouTubeLink" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "url": "url" }' ``` Aliases: messages.sendYoutubeLink, messages.sendYouTubeLink, sendYoutubeLink ## setCallMedia Prepare a replacement before releasing the working media. Canonical reference: /docs/reference/client/client#setcallmedia License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: - `media?: { camera?: any; microphone?: any; onMediaFailure?: "end" | "keep-open"; speaker?: any; takeover?: boolean }`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setCallMedia({ call: {}, }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/setCallMedia" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {} }' ``` Aliases: calls.setCallMedia ## setChatEphemeral Set ephemeral messages for chat Canonical reference: /docs/reference/client/client#setchatephemeral ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ - `ephemeral: boolean | number`: Ephemeral setting ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setChatEphemeral({ chatId: "447123456789@c.us", ephemeral: 1, }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/setEphemeral" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us", "ephemeral": 1 }' ``` Aliases: chats.setEphemeral ## setGroupDescription Set group description Canonical reference: /docs/reference/client/client#setgroupdescription ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `description: string`: New group description ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupDescription({ groupId: "447123456789-1445627445@g.us", description: "description", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/setDescription" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "description": "description" }' ``` Aliases: groups.setDescription ## setGroupEditToAdminsOnly Restrict group editing to admins only Canonical reference: /docs/reference/client/client#setgroupedittoadminsonly ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `onlyAdmins: boolean`: Admins only setting ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupEditToAdminsOnly({ groupId: "447123456789-1445627445@g.us", onlyAdmins: true, }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/setEditAdminsOnly" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "onlyAdmins": true }' ``` Aliases: groups.setEditAdminsOnly, groups.setEditToAdminsOnly ## setGroupIcon Set group icon Canonical reference: /docs/reference/client/client#setgroupicon ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `image: string`: Image data URL ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupIcon({ groupId: "447123456789-1445627445@g.us", image: "image", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/setIcon" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "image": "image" }' ``` Aliases: groups.setIcon ## setGroupTitle Set group title Canonical reference: /docs/reference/client/client#setgrouptitle ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `title: string`: New group title ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupTitle({ groupId: "447123456789-1445627445@g.us", title: "title", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/setTitle" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "title": "title" }' ``` Aliases: groups.setTitle ## setGroupToAdminsOnly Restrict group to admins only Canonical reference: /docs/reference/client/client#setgrouptoadminsonly ### Parameters - `groupId: string`: Group chat ID A WhatsApp group JID Branded type: GroupChatId Pattern: ^\d+(-\d+)?@g\.us$ - `onlyAdmins: boolean`: Admins only setting ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupToAdminsOnly({ groupId: "447123456789-1445627445@g.us", onlyAdmins: true, }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/groups/setAdminsOnly" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "onlyAdmins": true }' ``` Aliases: groups.setAdminsOnly, groups.setToAdminsOnly ## setMyName Set host name Canonical reference: /docs/reference/client/client#setmyname ### Parameters - `name: string`: Display name ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setMyName({ name: "OpenWA Bot", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/session/setName" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "name": "OpenWA Bot" }' ``` Aliases: session.setDisplayName, session.setMyName, session.setName, setDisplayName ## setMyStatus Set host status Canonical reference: /docs/reference/client/client#setmystatus ### Parameters - `statusText: string`: Status text ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setMyStatus({ statusText: "Available", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/session/setStatus" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "statusText": "Available" }' ``` Aliases: session.setMyStatus, session.setStatus, session.setStatusText, setStatusText ## setProfilePicture Set profile picture Canonical reference: /docs/reference/client/client#setprofilepicture License: insiders ### Parameters - `image: string | Buffer`: Image file (path, URL, DataURL, or Base64) Branded type: DataURL ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setProfilePicture({ image: "data:image/png;base64,iVBORw0KGgo...", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/session/setProfilePicture" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "image": "data:image/png;base64,iVBORw0KGgo..." }' ``` Aliases: session.setProfilePic, session.setProfilePicture, setProfilePic ## sharePhoneNumber Share the signed-in account’s own phone number with a direct LID chat using WhatsApp’s native protocol. No arbitrary phone number is accepted. Canonical reference: /docs/reference/client/client#sharephonenumber ### Parameters - `to: string`: Recipient chat ID The target chat for a send operation Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Message ID [MessageId](/docs/reference/client/return-types#messageid) In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sharePhoneNumber({ to: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/contacts/sharePhoneNumber" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us" }' ``` Aliases: contacts.sharePhoneNumber ## starMessage Star a message Canonical reference: /docs/reference/client/client#starmessage ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.starMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/messages/star" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: messages.star ## startCall Prepare media and place an outgoing audio or video call. Canonical reference: /docs/reference/client/client#startcall License: restricted ### Parameters - `to: string`: - `microphone?: unknown`: - `speaker?: unknown`: - `camera?: unknown`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.startCall({ to: "to", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/startCall" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "to" }' ``` Aliases: calls.startCall ## stopCallAudioObserver Stop an audio observer and finalize its sink. Canonical reference: /docs/reference/client/client#stopcallaudioobserver License: restricted ### Parameters - `call: { generation: string; id: string; sessionId: string }`: - `observerId: string`: ### Returns `Promise<{ ok: true; status: "requested"; callId?: string | null } | { ok: false; status: "unavailable" | "failed" | "unknown"; reason: { code: string; message: string; status: number | "Infinity" | "-Infinity" | "NaN" }; callId?: string>` Resolves to `{ ok: true; status: "requested"; callId?: string \| null } \| { ok: false; status: "unavailable" \| "failed" \| "unknown"; reason: { code: string; message: string; status: number \| "Infinity" \| "-Infinity" \| "NaN" }; callId?: string`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.stopCallAudioObserver({ call: {}, observerId: "observerId", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/calls/stopCallAudioObserver" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "call": {}, "observerId": "observerId" }' ``` Aliases: calls.stopCallAudioObserver ## unarchiveChat Unarchive a chat Canonical reference: /docs/reference/client/client#unarchivechat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unarchiveChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/unarchive" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.unarchive ## unblockContact Unblock a contact Canonical reference: /docs/reference/client/client#unblockcontact ### Parameters - `contactId: string`: Contact ID A WhatsApp contact JID Branded type: ContactId Pattern: ^\d+@c\.us$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unblockContact({ contactId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/contacts/unblock" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "contactId": "447123456789@c.us" }' ``` Aliases: contacts.contactUnblock, contacts.unblock, contactUnblock ## unmuteChat Unmute a chat Canonical reference: /docs/reference/client/client#unmutechat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `string \| boolean \| number`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unmuteChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/unmute" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.unmute ## unpinChat Unpin a chat Canonical reference: /docs/reference/client/client#unpinchat ### Parameters - `chatId: string`: Chat ID (phone number or formatted ID) A contact ID, group ID, or LID JID Branded type: ChatId Pattern: ^\d+(-\d+)?@(c|g)\.us$|^\d+@lid$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unpinChat({ chatId: "447123456789@c.us", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/chats/unpin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Aliases: chats.unpin ## unpinMessage Removes a message pin for everyone, subject to native chat permissions. Canonical reference: /docs/reference/client/client#unpinmessage ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unpinMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/unpin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: messages.unpin ## unstarMessage Unstar a message Canonical reference: /docs/reference/client/client#unstarmessage ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. Not declared on the in-process Client facade. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unstarMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ### HTTP ```bash curl -X PUT "http://localhost:8080/api/messages/unstar" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Aliases: messages.unstar ## voteOnPoll Votes using option IDs returned by getPollResults; an empty array removes the current vote. The poll must be open and allow this account to vote. Canonical reference: /docs/reference/client/client#voteonpoll ### Parameters - `messageId: string`: Message ID A WhatsApp message identifier Branded type: MessageId Pattern: ^(true|false)_.+_.+$ - `optionIds: number[]`: ### Returns `Promise` Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client: `Promise`. ### Node.js client ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.voteOnPoll({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", optionIds: [], }); ``` ### HTTP ```bash curl -X POST "http://localhost:8080/api/messages/voteOnPoll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "optionIds": [] }' ``` Aliases: messages.voteOnPoll # Communities Client API (/docs/reference/client/communities) # Communities Client API [#communities-client-api] This page documents 6 schema-registry client methods in the `communities` namespace. ## `getAllCommunities` [#getallcommunities] Get all communities **Signature** `client.getAllCommunities(): Promise` ### Usage [#usage] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllCommunities(); ``` ```bash curl -X GET "http://localhost:8080/api/communities/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters] This method does not define input parameters. ### Returns [#returns] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | GroupChatId | [View fields](/docs/reference/client/return-types#groupchatid) | Resolves to `GroupChatId[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Opaque group conversation identifier returned by a group lookup or join method.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------------ | | Namespace | `communities` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `communities.all`, `communities.getAll`, `communities.list`, `listCommunities` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ------------------- | ------ | | Primary | `GET` | `/api/communities/getAll` | `getAllCommunities` | Active | | Alias | `GET` | `/api/communities/all` | `communities.all` | Active | | Alias | `GET` | `/api/communities/list` | `communities.list` | Active | | Alias | `GET` | `/api/getAllCommunities` | `getAllCommunities` | Active | | Alias | `GET` | `/api/listCommunities` | `listCommunities` | Active |
## `getCommunityAdminIds` [#getcommunityadminids] Get community admin IDs **Signature** `client.getCommunityAdminIds(communityId: string): Promise` ### Usage [#usage-1] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityAdminIds({ communityId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/communities/getAdminIds" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | ----------------- | -------------------------------------------------------------------- | | Return type | `Promise` | | CommunityAdminIds | [View fields](/docs/reference/client/return-types#communityadminids) | Resolves to `CommunityAdminIds[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | ------------------------- | | Namespace | `communities` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `communityId` | | Aliases | `communities.getAdminIds` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------ | ---------------------- | ------ | | Primary | `GET` | `/api/communities/getAdminIds` | `getCommunityAdminIds` | Active | | Alias | `GET` | `/api/getCommunityAdminIds` | `getCommunityAdminIds` | Active |
## `getCommunityAdmins` [#getcommunityadmins] Get community admins **Signature** `client.getCommunityAdmins(communityId: string): Promise` ### Usage [#usage-2] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityAdmins({ communityId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/communities/getAdmins" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-2] ### Returns [#returns-2] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | CommunityAdmins | [View fields](/docs/reference/client/return-types#communityadmins) | Resolves to `CommunityAdmins[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | ----------------------- | | Namespace | `communities` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `communityId` | | Aliases | `communities.getAdmins` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | -------------------- | ------ | | Primary | `GET` | `/api/communities/getAdmins` | `getCommunityAdmins` | Active | | Alias | `GET` | `/api/getCommunityAdmins` | `getCommunityAdmins` | Active |
## `getCommunityInfo` [#getcommunityinfo] Get community metadata **Signature** `client.getCommunityInfo(communityId: string): Promise` ### Usage [#usage-3] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityInfo({ communityId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/communities/getInfo" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": { "id": "447123456789-1445627445@g.us", "subject": "example", "creation": 0, "participants": [], "subGroups": [] } } ``` ### Parameters [#parameters-3] ### Returns [#returns-3] | Prop | Value | | ----------------- | -------------------------------------------------------------------- | | Return type | `Promise` | | CommunityMetadata | [View fields](/docs/reference/client/return-types#communitymetadata) | Resolves to `CommunityMetadata`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | --------------------- | | Namespace | `communities` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `communityId` | | Aliases | `communities.getInfo` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | ------------------ | ------ | | Primary | `GET` | `/api/communities/getInfo` | `getCommunityInfo` | Active | | Alias | `GET` | `/api/getCommunityInfo` | `getCommunityInfo` | Active |
## `getCommunityParticipantIds` [#getcommunityparticipantids] Get community participant IDs **Signature** `client.getCommunityParticipantIds(communityId: string): Promise` ### Usage [#usage-4] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityParticipantIds({ communityId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/communities/getParticipantIds" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-4] ### Returns [#returns-4] | Prop | Value | | ----------------------- | -------------------------------------------------------------------------- | | Return type | `Promise` | | CommunityParticipantIds | [View fields](/docs/reference/client/return-types#communityparticipantids) | Resolves to `CommunityParticipantIds[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ------------------------------- | | Namespace | `communities` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `communityId` | | Aliases | `communities.getParticipantIds` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------------ | ---------------------------- | ------ | | Primary | `GET` | `/api/communities/getParticipantIds` | `getCommunityParticipantIds` | Active | | Alias | `GET` | `/api/getCommunityParticipantIds` | `getCommunityParticipantIds` | Active |
## `getCommunityParticipants` [#getcommunityparticipants] Get community participants **Signature** `client.getCommunityParticipants(communityId: string): Promise` ### Usage [#usage-5] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommunityParticipants({ communityId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/communities/getParticipants" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "communityId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-5] ### Returns [#returns-5] | Prop | Value | | --------------------- | ------------------------------------------------------------------------ | | Return type | `Promise` | | CommunityParticipants | [View fields](/docs/reference/client/return-types#communityparticipants) | Resolves to `CommunityParticipants[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | ----------------------------- | | Namespace | `communities` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `communityId` | | Aliases | `communities.getParticipants` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------------- | -------------------------- | ------ | | Primary | `GET` | `/api/communities/getParticipants` | `getCommunityParticipants` | Active | | Alias | `GET` | `/api/getCommunityParticipants` | `getCommunityParticipants` | Active |
# Contacts Client API (/docs/reference/client/contacts) # Contacts Client API [#contacts-client-api] This page documents 12 schema-registry client methods in the `contacts` namespace. ## `blockContact` [#blockcontact] Block a contact **Signature** `client.blockContact(contactId: string): Promise` ### Usage [#usage] ```ts const result = await client.blockContact("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.blockContact({ contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/contacts/block" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | --------------------------------------------------------- | | Namespace | `contacts` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `contactBlock`, `contacts.block`, `contacts.contactBlock` | | Deprecated aliases | `contactBlock`, `contacts.contactBlock` | | License | `none` | | WAPI override | `contactBlock` | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ---------------- | ------ | ---------------------------- | ----------------------- | -------------- | | Primary | `PUT` | `/api/contacts/block` | `blockContact` | Active | | Alias | `PUT` | `/api/blockContact` | `blockContact` | Active | | Deprecated alias | `PUT` | `/api/contactBlock` | `contactBlock` | **Deprecated** | | Deprecated alias | `PUT` | `/api/contacts/contactBlock` | `contacts.contactBlock` | **Deprecated** |
## `checkReadReceipts` [#checkreadreceipts] Check read receipts setting **Signature** `client.checkReadReceipts(contactId: string): Promise` ### Usage [#usage-2] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.checkReadReceipts({ contactId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/contacts/checkReadReceipts" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-2] ### Returns [#returns-2] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | --------------------------------------------------------------------------- | | Namespace | `contacts` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `contacts.checkReadReceipts`, `contacts.getReadReceipts`, `getReadReceipts` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------------- | -------------------------- | ------ | | Primary | `GET` | `/api/contacts/checkReadReceipts` | `checkReadReceipts` | Active | | Alias | `GET` | `/api/checkReadReceipts` | `checkReadReceipts` | Active | | Alias | `GET` | `/api/contacts/getReadReceipts` | `contacts.getReadReceipts` | Active | | Alias | `GET` | `/api/getReadReceipts` | `getReadReceipts` | Active |
## `getAllContacts` [#getallcontacts] Retrieves all contacts **Signature** `client.getAllContacts(): Promise` ### Usage [#usage-3] ```ts const result = await client.getAllContacts(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllContacts(); ``` ```bash curl -X GET "http://localhost:8080/api/contacts/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-3] This method does not define input parameters. ### Returns [#returns-3] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Contact | [View fields](/docs/reference/client/return-types#contact) | Resolves to `Contact[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | ------------------------------------------------------------------ | | Namespace | `contacts` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `contacts.all`, `contacts.getAll`, `contacts.list`, `listContacts` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------- | ---------------- | ------ | | Primary | `GET` | `/api/contacts/getAll` | `getAllContacts` | Active | | Alias | `GET` | `/api/contacts/all` | `contacts.all` | Active | | Alias | `GET` | `/api/contacts/list` | `contacts.list` | Active | | Alias | `GET` | `/api/getAllContacts` | `getAllContacts` | Active | | Alias | `GET` | `/api/listContacts` | `listContacts` | Active |
## `getBlockedIds` [#getblockedids] Get blocked contact IDs **Signature** `client.getBlockedIds(): Promise` ### Usage [#usage-4] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBlockedIds(); ``` ```bash curl -X GET "http://localhost:8080/api/contacts/getBlockedIds" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-4] This method does not define input parameters. ### Returns [#returns-4] | Prop | Value | | ----------- | ------------------------------------------------------------ | | Return type | `Promise` | | ContactId | [View fields](/docs/reference/client/return-types#contactid) | Resolves to `ContactId[] \| false`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ------------------------ | | Namespace | `contacts` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `contacts.getBlockedIds` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------------- | --------------- | ------ | | Primary | `GET` | `/api/contacts/getBlockedIds` | `getBlockedIds` | Active | | Alias | `GET` | `/api/getBlockedIds` | `getBlockedIds` | Active |
## `getCommonGroups` [#getcommongroups] Get common groups with contact **Signature** `client.getCommonGroups(contactId: string): Promise` ### Usage [#usage-5] ```ts const result = await client.getCommonGroups("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getCommonGroups({ contactId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/contacts/getCommonGroups" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-5] ### Returns [#returns-5] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | CommonGroup | [View fields](/docs/reference/client/return-types#commongroup) | Resolves to `CommonGroup[] \| false`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise>`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | -------------------------- | | Namespace | `contacts` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `contacts.getCommonGroups` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ----------------- | ------ | | Primary | `GET` | `/api/contacts/getCommonGroups` | `getCommonGroups` | Active | | Alias | `GET` | `/api/getCommonGroups` | `getCommonGroups` | Active |
## `getContact` [#getcontact] Get specific contact **Signature** `client.getContact(contactId: string): Promise` ### Usage [#usage-6] ```ts const result = await client.getContact("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getContact({ contactId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/contacts/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "contactId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": { "id": "447123456789@c.us" } } ``` ### Parameters [#parameters-6] ### Returns [#returns-6] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Contact | [View fields](/docs/reference/client/return-types#contact) | Resolves to `Contact \| null`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-6] | Prop | Value | | -------------------------- | -------------- | | Namespace | `contacts` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `contacts.get` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-6] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------- | ------------ | ------ | | Primary | `GET` | `/api/contacts/get` | `getContact` | Active | | Alias | `GET` | `/api/getContact` | `getContact` | Active |
## `getProfilePicture` [#getprofilepicture] Get profile picture from server **Signature** `client.getProfilePicture(chatId: string): Promise` ### Usage [#usage-8] ```ts const result = await client.getProfilePic("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getProfilePicture({ chatId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/contacts/getProfilePicture" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-8] ### Returns [#returns-8] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-8] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------------------------- | | Namespace | `contacts` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `contacts.getProfilePicFromServer`, `contacts.getProfilePicture`, `getProfilePicFromServer` | | Deprecated aliases | `contacts.getProfilePicFromServer`, `getProfilePicFromServer` | | License | `none` | | WAPI override | `getProfilePicFromServer` | | Deprecated | No | ### Routes [#routes-8] | Type | Method | Path | Name | Status | | ---------------- | ------ | --------------------------------------- | ---------------------------------- | -------------- | | Primary | `GET` | `/api/contacts/getProfilePicture` | `getProfilePicture` | Active | | Deprecated alias | `GET` | `/api/contacts/getProfilePicFromServer` | `contacts.getProfilePicFromServer` | **Deprecated** | | Deprecated alias | `GET` | `/api/getProfilePicFromServer` | `getProfilePicFromServer` | **Deprecated** | | Alias | `GET` | `/api/getProfilePicture` | `getProfilePicture` | Active |
## `requestPhoneNumber` [#requestphonenumber] Ask a direct LID chat to voluntarily share their phone number. A submitted request does not confirm sharing. **Signature** `client.requestPhoneNumber(to: string): Promise` ### Usage [#usage-9] ```ts const result = await client.requestPhoneNumber("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.requestPhoneNumber({ to: "447123456789@c.us", }); ``` ```bash curl -X POST "http://localhost:8080/api/contacts/requestPhoneNumber" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-9] ### Returns [#returns-9] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Message ID In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Message ID
Advanced method metadata ### Overview [#overview-9] | Prop | Value | | -------------------------- | ----------------------------- | | Namespace | `contacts` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to` | | Aliases | `contacts.requestPhoneNumber` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-9] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------------- | -------------------- | ------ | | Primary | `POST` | `/api/contacts/requestPhoneNumber` | `requestPhoneNumber` | Active | | Alias | `POST` | `/api/requestPhoneNumber` | `requestPhoneNumber` | Active |
## `sharePhoneNumber` [#sharephonenumber] Share the signed-in account’s own phone number with a direct LID chat using WhatsApp’s native protocol. No arbitrary phone number is accepted. **Signature** `client.sharePhoneNumber(to: string): Promise` ### Usage [#usage-10] ```ts const result = await client.sharePhoneNumber("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sharePhoneNumber({ to: "447123456789@c.us", }); ``` ```bash curl -X POST "http://localhost:8080/api/contacts/sharePhoneNumber" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-10] ### Returns [#returns-10] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Message ID In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Message ID
Advanced method metadata ### Overview [#overview-10] | Prop | Value | | -------------------------- | --------------------------- | | Namespace | `contacts` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to` | | Aliases | `contacts.sharePhoneNumber` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-10] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------------- | ------------------ | ------ | | Primary | `POST` | `/api/contacts/sharePhoneNumber` | `sharePhoneNumber` | Active | | Alias | `POST` | `/api/sharePhoneNumber` | `sharePhoneNumber` | Active |
## `unblockContact` [#unblockcontact] Unblock a contact **Signature** `client.unblockContact(contactId: string): Promise` ### Usage [#usage-11] ```ts const result = await client.unblockContact("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unblockContact({ contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/contacts/unblock" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-11] ### Returns [#returns-11] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-11] | Prop | Value | | -------------------------- | --------------------------------------------------------------- | | Namespace | `contacts` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `contactId` | | Aliases | `contacts.contactUnblock`, `contacts.unblock`, `contactUnblock` | | Deprecated aliases | `contacts.contactUnblock`, `contactUnblock` | | License | `none` | | WAPI override | `contactUnblock` | | Deprecated | No | ### Routes [#routes-11] | Type | Method | Path | Name | Status | | ---------------- | ------ | ------------------------------ | ------------------------- | -------------- | | Primary | `PUT` | `/api/contacts/unblock` | `unblockContact` | Active | | Deprecated alias | `PUT` | `/api/contacts/contactUnblock` | `contacts.contactUnblock` | **Deprecated** | | Deprecated alias | `PUT` | `/api/contactUnblock` | `contactUnblock` | **Deprecated** | | Alias | `PUT` | `/api/unblockContact` | `unblockContact` | Active |
# Events (/docs/reference/client/events) # Events [#events] open-wa emits **17** events. Subscribe with the client helper (`client.onMessage`), the legacy listener name, or the event bus wildcard. Payload types are derived from each event's registered schema. | Event | Legacy listener | Description | Payload | Status | License | | ------------------------------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------ | | `ack` | `onAck` | Fired when a message acknowledgment is received | `{ id: MessageId; ack: "ACK_PENDING" \| "ACK_SERVER" \| "ACK_DEVICE" \| "ACK_READ" \| "ACK_PLAYED" \| -1 \| "ACK_ERROR" \| 0 \| 1 \| 2 \| 3 \| 4 }` | stable | - | | `addedToGroup` | `onAddedToGroup` | Fired when you are added to a group | `{ groupId: string; groupName: string; addedBy?: string }` | stable | - | | `anyMessage` | `onAnyMessage` | Fired for any message including own messages | `Message` | stable | - | | `callState` | `onCallState` | Observed native call state, including unknown states. | `{ id: string; sessionId: string; generation: string; peer: string; state: string; direction: "incoming" \| "outgoing"; video: boolean; terminal: boolean; observedAt: number \| "Infinity" \| "-Infinity" \| "NaN"; endReason?: { code: string; message: string; source: "local" \| "media" \| "license" \| "session" \| "whatsapp" }; media?: { state: "attached" \| "ended"; reason?: string } }` | experimental | `restricted` | | `chatState` | `onChatState` | Fired when someone is typing or recording | `{ chatId: string; state: "typing" \| "recording" \| "paused"; senderId: string }` | stable | - | | `event` | `onEvent` | A native event added or changed in the synced message store, including this account’s events. | `WhatsAppEvent` | beta | - | | `eventResponse` | `onEventResponse` | A decrypted RSVP added or changed in WhatsApp’s dedicated event-response collection. History loads may emit responses too; deduplicate by id and timestamp. | `EventResponse` | beta | - | | `formResponse` | `onFormResponse` | A form submission with question IDs and chosen option IDs | `FormResponse` | stable | `insiders` | | `incomingCall` | `onIncomingCall` | A native incoming call. The SDK hydrates its bound controls. | `{ id: string; sessionId: string; generation: string; peer: string; state: string; direction: "incoming" \| "outgoing"; video: boolean; terminal: boolean; observedAt: number \| "Infinity" \| "-Infinity" \| "NaN"; endReason?: { code: string; message: string; source: "local" \| "media" \| "license" \| "session" \| "whatsapp" }; media?: { state: "attached" \| "ended"; reason?: string } }` | experimental | `restricted` | | `interactiveResponse` | `onInteractiveResponse` | Structured reply, list, form or native interactive response | `InteractiveResponse` | stable | `insiders` | | `logout` | `onLogout` | Fired when the session is logged out | `{ reason?: string; timestamp: number }` | stable | - | | `message` | `onMessage` | Fired when a new message is received (excluding own messages) | `Message` | stable | - | | `messageDeleted` | `onMessageDeleted` | Fired when a message is deleted | `Message` | stable | - | | `participantsChanged` | `onParticipantsChanged` | Fired when group participants change | `{ groupId: GroupId; action: "add" \| "remove" \| "promote" \| "demote" \| "invite" \| "leave"; participantIds: ContactId[]; by?: string; who?: ContactId; chat?: GroupId \| { _serialized: GroupId } }` | stable | - | | `pollVote` | `onPollVote` | A decrypted poll vote added or changed in the synced store. Hydration can emit old votes. Hidden-voter polls omit sender and message identifiers. Deduplicate ordinary votes by id and timestamp; refresh getPollResults for current totals. | `PollVote` | beta | - | | `reaction` | `onReaction` | Fired when a reaction is added to a message | `{ messageId: string; reaction: string; senderId: string; timestamp: number }` | stable | - | | `stateChanged` | `onStateChanged` | Fired when connection state changes | `{ state: "STARTING" \| "AUTHENTICATING" \| "READY" \| "STOPPED" \| "CONNECTED" \| "DISCONNECTED" \| "SYNCING" \| "CONFLICT" \| "UNLAUNCHED" \| "PAIRING" \| "TIMEOUT"; previousState?: "STARTING" \| "AUTHENTICATING" \| "READY" \| "STOPPED" \| "CONNECTED" \| "DISCONNECTED" \| "SYNCING" \| "CONFLICT" \| "UNLAUNCHED" \| "PAIRING" \| "TIMEOUT" }` | stable | - | # Generated types (/docs/reference/client/generated-types) # Generated types [#generated-types] `gen-types.ts` emits input and output type aliases for every method from its `inputSchema` and `outputSchema` into `packages/schema/src/generated/types.ts`. `@open-wa/wa-automate-types-only` re-exposes this generated surface so consumers can import method types without the runtime. # Groups Client API (/docs/reference/client/groups) # Groups Client API [#groups-client-api] This page documents 22 schema-registry client methods in the `groups` namespace. ## `addParticipant` [#addparticipant] Add participant to group **Signature** `client.addParticipant(groupId: string, contactId: string): Promise` ### Usage [#usage] ```ts const result = await client.addParticipant("447123456789-1445627445@g.us", "447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.addParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/addParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | ---------------------------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `contactId` | | Aliases | `addGroupParticipant`, `groups.addParticipant` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | --------------------- | ------ | | Primary | `PUT` | `/api/groups/addParticipant` | `addParticipant` | Active | | Alias | `PUT` | `/api/addGroupParticipant` | `addGroupParticipant` | Active | | Alias | `PUT` | `/api/addParticipant` | `addParticipant` | Active |
## `approveGroupJoinRequest` [#approvegroupjoinrequest] Approve group join request **Signature** `client.approveGroupJoinRequest(groupId: string, contactId: string): Promise` ### Usage [#usage-1] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.approveGroupJoinRequest({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/approveJoinRequest" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `string \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | --------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `contactId` | | Aliases | `groups.approveJoinRequest` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------------- | ------------------------- | ------ | | Primary | `PUT` | `/api/groups/approveJoinRequest` | `approveGroupJoinRequest` | Active | | Alias | `PUT` | `/api/approveGroupJoinRequest` | `approveGroupJoinRequest` | Active |
## `demoteParticipant` [#demoteparticipant] Demote participant from admin **Signature** `client.demoteParticipant(groupId: string, contactId: string): Promise` ### Usage [#usage-3] ```ts const result = await client.demoteParticipant("447123456789-1445627445@g.us", "447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.demoteParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/demoteParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-3] ### Returns [#returns-3] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | ---------------------------------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `contactId` | | Aliases | `demoteGroupParticipant`, `groups.demoteParticipant` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ------------------------ | ------ | | Primary | `PUT` | `/api/groups/demoteParticipant` | `demoteParticipant` | Active | | Alias | `PUT` | `/api/demoteGroupParticipant` | `demoteGroupParticipant` | Active | | Alias | `PUT` | `/api/demoteParticipant` | `demoteParticipant` | Active |
## `getAllGroups` [#getallgroups] Get all groups **Signature** `client.getAllGroups(withNewMessagesOnly?: boolean): Promise` ### Usage [#usage-4] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllGroups(); ``` ```bash curl -X GET "http://localhost:8080/api/groups/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-4] ### Returns [#returns-4] | Prop | Value | | ----------- | ------------------------------------------------------- | | Return type | `Promise` | | Chat | [View fields](/docs/reference/client/return-types#chat) | Resolves to `Chat[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Serialized chat returned by chat lookups and listings. Its id is an opaque ChatId; additional WhatsApp Web fields may be present.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ---------------------------------------------------------- | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `withNewMessagesOnly` | | Aliases | `groups.all`, `groups.getAll`, `groups.list`, `listGroups` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | -------------- | ------ | | Primary | `GET` | `/api/groups/getAll` | `getAllGroups` | Active | | Alias | `GET` | `/api/getAllGroups` | `getAllGroups` | Active | | Alias | `GET` | `/api/groups/all` | `groups.all` | Active | | Alias | `GET` | `/api/groups/list` | `groups.list` | Active | | Alias | `GET` | `/api/listGroups` | `listGroups` | Active |
## `getGroupAdmins` [#getgroupadmins] Get group admins **Signature** `client.getGroupAdmins(groupId: string): Promise` ### Usage [#usage-5] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupAdmins({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/groups/getAdmins" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-5] ### Returns [#returns-5] | Prop | Value | | ----------- | ------------------------------------------------------------ | | Return type | `Promise` | | ContactId | [View fields](/docs/reference/client/return-types#contactid) | Resolves to `ContactId[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Opaque WhatsApp contact identifier. Preserve the exact value returned by the API.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `groups.getAdmins` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ---------------- | ------ | | Primary | `GET` | `/api/groups/getAdmins` | `getGroupAdmins` | Active | | Alias | `GET` | `/api/getGroupAdmins` | `getGroupAdmins` | Active |
## `getGroupInfo` [#getgroupinfo] Get the group title and description through the installed private patch **Signature** `client.getGroupInfo(groupId: string): Promise<{ title: string; description?: string \| null } \| string \| null>` ### Usage [#usage-6] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupInfo({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/groups/getInfo" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": { "title": "example" } } ``` ### Parameters [#parameters-6] ### Returns [#returns-6] | Prop | Value | | ----------- | ---------------------------------------------------------------------------- | | Return type | `Promise<{ title: string; description?: string \| null } \| string \| null>` | Resolves to `{ title: string; description?: string \| null } \| string \| null`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-6] | Prop | Value | | -------------------------- | ---------------- | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `groups.getInfo` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-6] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | -------------- | ------ | | Primary | `GET` | `/api/groups/getInfo` | `getGroupInfo` | Active | | Alias | `GET` | `/api/getGroupInfo` | `getGroupInfo` | Active |
## `getGroupInviteLink` [#getgroupinvitelink] Get group invite link **Signature** `client.getGroupInviteLink(groupId: string): Promise` ### Usage [#usage-7] ```ts const result = await client.getGroupInviteLink("447123456789-1445627445@g.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupInviteLink({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/groups/getInviteLink" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-7] ### Returns [#returns-7] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-7] | Prop | Value | | -------------------------- | ---------------------- | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `groups.getInviteLink` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-7] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------- | -------------------- | ------ | | Primary | `GET` | `/api/groups/getInviteLink` | `getGroupInviteLink` | Active | | Alias | `GET` | `/api/getGroupInviteLink` | `getGroupInviteLink` | Active |
## `getGroupMembers` [#getgroupmembers] Retrieves members of a group **Signature** `client.getGroupMembers(groupId: string): Promise` ### Usage [#usage-8] ```ts const result = await client.getGroupMembers("447123456789-1445627445@g.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupMembers({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/groups/getMembers" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-8] ### Returns [#returns-8] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Contact | [View fields](/docs/reference/client/return-types#contact) | Resolves to `Contact[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-8] | Prop | Value | | -------------------------- | ------------------- | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `groups.getMembers` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-8] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ----------------- | ------ | | Primary | `GET` | `/api/groups/getMembers` | `getGroupMembers` | Active | | Alias | `GET` | `/api/getGroupMembers` | `getGroupMembers` | Active |
## `getGroupMembersId` [#getgroupmembersid] Get group member IDs **Signature** `client.getGroupMembersId(groupId: string): Promise` ### Usage [#usage-9] ```ts const result = await client.getGroupMembersId("447123456789-1445627445@g.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getGroupMembersId({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -G "http://localhost:8080/api/groups/getMemberIds" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "groupId=447123456789-1445627445@g.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-9] ### Returns [#returns-9] | Prop | Value | | ----------- | ------------------------------------------------------------ | | Return type | `Promise` | | ContactId | [View fields](/docs/reference/client/return-types#contactid) | Resolves to `ContactId[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Opaque WhatsApp contact identifier. Preserve the exact value returned by the API.
Advanced method metadata ### Overview [#overview-9] | Prop | Value | | -------------------------- | ----------------------------------------------------------------- | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `getGroupMemberIds`, `groups.getMemberIds`, `groups.getMembersId` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-9] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | --------------------- | ------ | | Primary | `GET` | `/api/groups/getMemberIds` | `getGroupMembersId` | Active | | Alias | `GET` | `/api/getGroupMemberIds` | `getGroupMemberIds` | Active | | Alias | `GET` | `/api/getGroupMembersId` | `getGroupMembersId` | Active | | Alias | `GET` | `/api/groups/getMembersId` | `groups.getMembersId` | Active |
## `getKickedGroups` [#getkickedgroups] Get groups where kicked **Signature** `client.getKickedGroups(): Promise` ### Usage [#usage-10] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getKickedGroups(); ``` ```bash curl -X GET "http://localhost:8080/api/groups/getKicked" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-10] This method does not define input parameters. ### Returns [#returns-10] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | GroupChatId | [View fields](/docs/reference/client/return-types#groupchatid) | Resolves to `GroupChatId[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Opaque group conversation identifier returned by a group lookup or join method.
Advanced method metadata ### Overview [#overview-10] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `groups` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `groups.getKicked` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-10] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ----------------- | ------ | | Primary | `GET` | `/api/groups/getKicked` | `getKickedGroups` | Active | | Alias | `GET` | `/api/getKickedGroups` | `getKickedGroups` | Active |
## `leaveGroup` [#leavegroup] Leave a group **Signature** `client.leaveGroup(groupId: string): Promise` ### Usage [#usage-12] ```ts const result = await client.leaveGroup("447123456789-1445627445@g.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.leaveGroup({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/leave" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us" }' ``` Example response envelope: ```json { "success": true } ``` ### Parameters [#parameters-12] ### Returns [#returns-12] | Prop | Value | | ----------- | --------------- | | Return type | `Promise` | Resolves to `void`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-12] | Prop | Value | | -------------------------- | -------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `groups.leave` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-12] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------- | ------------ | ------ | | Primary | `PUT` | `/api/groups/leave` | `leaveGroup` | Active | | Alias | `PUT` | `/api/leaveGroup` | `leaveGroup` | Active |
## `promoteParticipant` [#promoteparticipant] Promote participant to admin **Signature** `client.promoteParticipant(groupId: string, contactId: string): Promise` ### Usage [#usage-13] ```ts const result = await client.promoteParticipant("447123456789-1445627445@g.us", "447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.promoteParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/promoteParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-13] ### Returns [#returns-13] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `boolean \| string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-13] | Prop | Value | | -------------------------- | ------------------------------------------------------ | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `contactId` | | Aliases | `groups.promoteParticipant`, `promoteGroupParticipant` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-13] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------------- | ------------------------- | ------ | | Primary | `PUT` | `/api/groups/promoteParticipant` | `promoteParticipant` | Active | | Alias | `PUT` | `/api/promoteGroupParticipant` | `promoteGroupParticipant` | Active | | Alias | `PUT` | `/api/promoteParticipant` | `promoteParticipant` | Active |
## `rejectGroupJoinRequest` [#rejectgroupjoinrequest] Reject group join request **Signature** `client.rejectGroupJoinRequest(groupId: string, contactId: string): Promise` ### Usage [#usage-14] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.rejectGroupJoinRequest({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/rejectJoinRequest" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-14] ### Returns [#returns-14] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `string \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-14] | Prop | Value | | -------------------------- | -------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `contactId` | | Aliases | `groups.rejectJoinRequest` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-14] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ------------------------ | ------ | | Primary | `PUT` | `/api/groups/rejectJoinRequest` | `rejectGroupJoinRequest` | Active | | Alias | `PUT` | `/api/rejectGroupJoinRequest` | `rejectGroupJoinRequest` | Active |
## `removeParticipant` [#removeparticipant] Remove participant from group **Signature** `client.removeParticipant(groupId: string, contactId: string): Promise` ### Usage [#usage-15] ```ts const result = await client.removeParticipant("447123456789-1445627445@g.us", "447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.removeParticipant({ groupId: "447123456789-1445627445@g.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/removeParticipant" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-15] ### Returns [#returns-15] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-15] | Prop | Value | | -------------------------- | ---------------------------------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `contactId` | | Aliases | `groups.removeParticipant`, `removeGroupParticipant` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-15] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ------------------------ | ------ | | Primary | `PUT` | `/api/groups/removeParticipant` | `removeParticipant` | Active | | Alias | `PUT` | `/api/removeGroupParticipant` | `removeGroupParticipant` | Active | | Alias | `PUT` | `/api/removeParticipant` | `removeParticipant` | Active |
## `revokeGroupInviteLink` [#revokegroupinvitelink] Revoke group invite link **Signature** `client.revokeGroupInviteLink(groupId: string): Promise` ### Usage [#usage-16] ```ts const result = await client.revokeGroupInviteLink("447123456789-1445627445@g.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.revokeGroupInviteLink({ groupId: "447123456789-1445627445@g.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/revokeInviteLink" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us" }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-16] ### Returns [#returns-16] | Prop | Value | | ----------- | ---------------------------- | | Return type | `Promise` | Resolves to `string \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-16] | Prop | Value | | -------------------------- | ------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId` | | Aliases | `groups.revokeInviteLink` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-16] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------ | ----------------------- | ------ | | Primary | `PUT` | `/api/groups/revokeInviteLink` | `revokeGroupInviteLink` | Active | | Alias | `PUT` | `/api/revokeGroupInviteLink` | `revokeGroupInviteLink` | Active |
## `setGroupDescription` [#setgroupdescription] Set group description **Signature** `client.setGroupDescription(groupId: string, description: string): Promise` ### Usage [#usage-17] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupDescription({ groupId: "447123456789-1445627445@g.us", description: "description", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/setDescription" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "description": "description" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-17] ### Returns [#returns-17] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-17] | Prop | Value | | -------------------------- | ------------------------ | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `description` | | Aliases | `groups.setDescription` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-17] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | --------------------- | ------ | | Primary | `PUT` | `/api/groups/setDescription` | `setGroupDescription` | Active | | Alias | `PUT` | `/api/setGroupDescription` | `setGroupDescription` | Active |
## `setGroupEditToAdminsOnly` [#setgroupedittoadminsonly] Restrict group editing to admins only **Signature** `client.setGroupEditToAdminsOnly(groupId: string, onlyAdmins: boolean): Promise` ### Usage [#usage-18] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupEditToAdminsOnly({ groupId: "447123456789-1445627445@g.us", onlyAdmins: true, }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/setEditAdminsOnly" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "onlyAdmins": true }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-18] ### Returns [#returns-18] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-18] | Prop | Value | | -------------------------- | -------------------------------------------------------- | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `onlyAdmins` | | Aliases | `groups.setEditAdminsOnly`, `groups.setEditToAdminsOnly` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-18] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------------- | ---------------------------- | ------ | | Primary | `PUT` | `/api/groups/setEditAdminsOnly` | `setGroupEditToAdminsOnly` | Active | | Alias | `PUT` | `/api/groups/setEditToAdminsOnly` | `groups.setEditToAdminsOnly` | Active | | Alias | `PUT` | `/api/setGroupEditToAdminsOnly` | `setGroupEditToAdminsOnly` | Active |
## `setGroupIcon` [#setgroupicon] Set group icon **Signature** `client.setGroupIcon(groupId: string, image: string): Promise` ### Usage [#usage-19] ```ts const result = await client.setGroupIcon("447123456789-1445627445@g.us", "image"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupIcon({ groupId: "447123456789-1445627445@g.us", image: "image", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/setIcon" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "image": "image" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-19] ### Returns [#returns-19] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-19] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `image` | | Aliases | `groups.setIcon` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-19] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | -------------- | ------ | | Primary | `PUT` | `/api/groups/setIcon` | `setGroupIcon` | Active | | Alias | `PUT` | `/api/setGroupIcon` | `setGroupIcon` | Active |
## `setGroupTitle` [#setgrouptitle] Set group title **Signature** `client.setGroupTitle(groupId: string, title: string): Promise` ### Usage [#usage-20] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupTitle({ groupId: "447123456789-1445627445@g.us", title: "title", }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/setTitle" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "title": "title" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-20] ### Returns [#returns-20] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-20] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `title` | | Aliases | `groups.setTitle` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-20] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------- | --------------- | ------ | | Primary | `PUT` | `/api/groups/setTitle` | `setGroupTitle` | Active | | Alias | `PUT` | `/api/setGroupTitle` | `setGroupTitle` | Active |
## `setGroupToAdminsOnly` [#setgrouptoadminsonly] Restrict group to admins only **Signature** `client.setGroupToAdminsOnly(groupId: string, onlyAdmins: boolean): Promise` ### Usage [#usage-21] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setGroupToAdminsOnly({ groupId: "447123456789-1445627445@g.us", onlyAdmins: true, }); ``` ```bash curl -X PUT "http://localhost:8080/api/groups/setAdminsOnly" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "groupId": "447123456789-1445627445@g.us", "onlyAdmins": true }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-21] ### Returns [#returns-21] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-21] | Prop | Value | | -------------------------- | ------------------------------------------------ | | Namespace | `groups` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `groupId`, `onlyAdmins` | | Aliases | `groups.setAdminsOnly`, `groups.setToAdminsOnly` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-21] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------------- | ------------------------ | ------ | | Primary | `PUT` | `/api/groups/setAdminsOnly` | `setGroupToAdminsOnly` | Active | | Alias | `PUT` | `/api/groups/setToAdminsOnly` | `groups.setToAdminsOnly` | Active | | Alias | `PUT` | `/api/setGroupToAdminsOnly` | `setGroupToAdminsOnly` | Active |
# Client API Reference (/docs/reference/client) # Client API Reference [#client-api-reference] `packages/schema/scripts/gen-client-reference-docs.ts` generates these pages from the schema registry and method files. ## Namespaces [#namespaces] 3 client methods 11 client methods 1 client methods 15 client methods 6 client methods 12 client methods 7 client methods 22 client methods 4 client methods 2 client methods 47 client methods 1 client methods 17 client methods 6 client methods ## How the client works [#how-the-client-works] * [Schema pipeline](/docs/reference/client/schema-pipeline) — one definition, many projections * [BaseClient](/docs/reference/client/base-client) — the generated flat client and dispatch table * [Namespaced client](/docs/reference/client/namespaced-client) — namespace member mapping * [Argument normalization](/docs/reference/client/argument-normalization) — positional vs object args, aliases * [Aliases](/docs/reference/client/aliases) — function and parameter key aliases * [Data models](/docs/reference/client/schemas) — Message, Contact, Chat, GroupMetadata * [Generated types](/docs/reference/client/generated-types) — where input/output type aliases come from * [Worked example: sendText](/docs/reference/client/send-text-worked-example) * [Events](/docs/reference/client/events) — registered events and payloads * [Licensed methods](/docs/reference/client/licensed-methods) — what needs a license key * [All client methods](/docs/reference/client/client) — alphabetical index # Labels Client API (/docs/reference/client/labels) # Labels Client API [#labels-client-api] This page documents 4 schema-registry client methods in the `labels` namespace. ## `addLabel` [#addlabel] Add label to chat **Signature** `client.addLabel(label: string, chatId: string): Promise` ### Usage [#usage] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.addLabel({ label: "label", chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/labels/add" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "label": "label", "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | ----------------- | | Namespace | `labels` | | Action | `update` | | Functionality | `business-only` | | Positional parameter order | `label`, `chatId` | | Aliases | `labels.add` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------- | ---------- | ------ | | Primary | `PUT` | `/api/labels/add` | `addLabel` | Active | | Alias | `PUT` | `/api/addLabel` | `addLabel` | Active |
## `getAllLabels` [#getalllabels] Get all labels **Signature** `client.getAllLabels(): Promise` ### Usage [#usage-1] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllLabels(); ``` ```bash curl -X GET "http://localhost:8080/api/labels/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-1] This method does not define input parameters. ### Returns [#returns-1] | Prop | Value | | ----------- | -------------------------------------------------------- | | Return type | `Promise` | | Label | [View fields](/docs/reference/client/return-types#label) | Resolves to `Label[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | ---------------------------------------------------------- | | Namespace | `labels` | | Action | `read` | | Functionality | `business-only` | | Positional parameter order | - | | Aliases | `labels.all`, `labels.getAll`, `labels.list`, `listLabels` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | -------------- | ------ | | Primary | `GET` | `/api/labels/getAll` | `getAllLabels` | Active | | Alias | `GET` | `/api/getAllLabels` | `getAllLabels` | Active | | Alias | `GET` | `/api/labels/all` | `labels.all` | Active | | Alias | `GET` | `/api/labels/list` | `labels.list` | Active | | Alias | `GET` | `/api/listLabels` | `listLabels` | Active |
## `getChatsByLabel` [#getchatsbylabel] Get chats by label **Signature** `client.getChatsByLabel(label: string): Promise` ### Usage [#usage-2] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getChatsByLabel({ label: "label", }); ``` ```bash curl -G "http://localhost:8080/api/labels/getChats" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "label=label" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-2] ### Returns [#returns-2] | Prop | Value | | ----------- | ------------------------------------------------------- | | Return type | `Promise` | | Chat | [View fields](/docs/reference/client/return-types#chat) | Resolves to `Chat[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Serialized chat returned by chat lookups and listings. Its id is an opaque ChatId; additional WhatsApp Web fields may be present.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | -------------------------------------- | | Namespace | `labels` | | Action | `read` | | Functionality | `business-only` | | Positional parameter order | `label` | | Aliases | `labels.getChats`, `labels.getChatsBy` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ------------------- | ------ | | Primary | `GET` | `/api/labels/getChats` | `getChatsByLabel` | Active | | Alias | `GET` | `/api/getChatsByLabel` | `getChatsByLabel` | Active | | Alias | `GET` | `/api/labels/getChatsBy` | `labels.getChatsBy` | Active |
## `removeLabel` [#removelabel] Remove label from chat **Signature** `client.removeLabel(label: string, chatId: string): Promise` ### Usage [#usage-3] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.removeLabel({ label: "label", chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/labels/remove" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "label": "label", "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-3] ### Returns [#returns-3] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | ----------------- | | Namespace | `labels` | | Action | `update` | | Functionality | `business-only` | | Positional parameter order | `label`, `chatId` | | Aliases | `labels.remove` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | ------------- | ------ | | Primary | `PUT` | `/api/labels/remove` | `removeLabel` | Active | | Alias | `PUT` | `/api/removeLabel` | `removeLabel` | Active |
# Licensed methods (/docs/reference/client/licensed-methods) # Licensed methods [#licensed-methods] The method registry marks **29** methods as licensed. The tables group them by required tier; check each method’s reference for runtime and account requirements. ## `insiders` (17) [#insiders-17] | Method | Namespace | Description | | -------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------- | | [`checkNumberStatus`](/docs/reference/client/client#checknumberstatus) | `contacts` | Check if number is on WhatsApp | | [`deleteAllStatus`](/docs/reference/client/client#deleteallstatus) | `status` | Delete all statuses | | [`deleteStatus`](/docs/reference/client/client#deletestatus) | `status` | Delete status | | [`getNumberProfile`](/docs/reference/client/client#getnumberprofile) | `contacts` | Get profile of number | | [`getOrder`](/docs/reference/client/client#getorder) | `business` | Get order details | | [`getStarredMessages`](/docs/reference/client/client#getstarredmessages) | `messages` | Get starred messages | | [`getStatus`](/docs/reference/client/client#getstatus) | `mystatus` | Get specific status | | [`getStories`](/docs/reference/client/client#getstories) | `status` | Get all stories | | [`postImageStatus`](/docs/reference/client/client#postimagestatus) | `status` | Post image status | | [`postTextStatus`](/docs/reference/client/client#posttextstatus) | `status` | Post text status | | [`postVideoStatus`](/docs/reference/client/client#postvideostatus) | `status` | Post video status | | [`sendAdvancedButtons`](/docs/reference/client/client#sendadvancedbuttons) | `messages` | Sends an advanced button message with media (deprecated) | | [`sendButtons`](/docs/reference/client/client#sendbuttons) | `messages` | Sends a button message (deprecated) | | [`sendInteractive`](/docs/reference/client/client#sendinteractive) | `messages` | Send a portable interactive message document (Insiders and above) | | [`sendListMessage`](/docs/reference/client/client#sendlistmessage) | `messages` | Sends a list/menu message (deprecated) | | [`sendRawMessage`](/docs/reference/client/client#sendrawmessage) | `messages` | Send a constructed WhatsApp protobuf Message payload (Insiders and above); media must already be uploaded | | [`setProfilePicture`](/docs/reference/client/client#setprofilepicture) | `session` | Set profile picture | ## `restricted` (12) [#restricted-12] | Method | Namespace | Description | | ------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------ | | [`acceptCall`](/docs/reference/client/client#acceptcall) | `calls` | Prepare media and accept this incoming call. | | [`clearCallAudio`](/docs/reference/client/client#clearcallaudio) | `calls` | Discard queued outgoing audio. | | [`createGroup`](/docs/reference/client/client#creategroup) | `groups` | Create new group | | [`endCall`](/docs/reference/client/client#endcall) | `calls` | Request hangup for this call. | | [`getActiveCall`](/docs/reference/client/client#getactivecall) | `calls` | Get the current call snapshot. SDK clients hydrate its bound methods. | | [`joinGroupViaLink`](/docs/reference/client/client#joingroupvialink) | `groups` | Join group via invite link | | [`muteCall`](/docs/reference/client/client#mutecall) | `calls` | Mute or unmute the microphone without ending reception. | | [`observeCallAudio`](/docs/reference/client/client#observecallaudio) | `calls` | Attach an isolated receive-only audio sink. Its failure does not interrupt the call owner. | | [`rejectCall`](/docs/reference/client/client#rejectcall) | `calls` | Reject this call with native state confirmation. | | [`setCallMedia`](/docs/reference/client/client#setcallmedia) | `calls` | Prepare a replacement before releasing the working media. | | [`startCall`](/docs/reference/client/client#startcall) | `calls` | Prepare media and place an outgoing audio or video call. | | [`stopCallAudioObserver`](/docs/reference/client/client#stopcallaudioobserver) | `calls` | Stop an audio observer and finalize its sink. | # Media Client API (/docs/reference/client/media) # Media Client API [#media-client-api] This page documents 2 schema-registry client methods in the `media` namespace. ## `decryptMedia` [#decryptmedia] Decrypts media from a message **Signature** `client.decryptMedia(message: { ack: -1 \| 0 \| 1 \| 2 \| 3 \| 4; associationType?: string; author?: string; body: string; broadcast?: boolean; caption?: string; chat: { ack?: any; contact: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; formattedTitle?: string; groupMetadata?: { announce?: boolean; creation: number; desc?: string; descOwner?: string; descTime?: number; id: string; owner?: string; participants: { id: string; isAdmin: boolean; isSuperAdmin?: boolean }[]; restrict?: boolean; subject: string }; id: string; isGroup: boolean; isReadOnly?: boolean; lastReceivedKey?: any; msgs?: any[]; muteExpiration?: number; name?: string; notSpam?: boolean; pin?: number; presence?: object; t?: number; unreadCount?: number }; chatId: string; clientUrl?: string; content: string; deprecatedMms3Url?: string; event?: { chatId: string; creatorId: string; description?: string; endTime?: number; extraGuestsAllowed?: boolean; fromMe: boolean; id: string; isCanceled: boolean; isScheduledCall?: boolean; joinLink?: string; location?: { latitude?: number \| "Infinity" \| "-Infinity" \| "NaN"; longitude?: number \| "Infinity" \| "-Infinity" \| "NaN"; name?: string }; name: string; startTime: number; timestamp: number }; expectedImageCount?: number; expectedVideoCount?: number; from: string; fromMe: boolean; id: string; invis?: boolean; isForwarded?: boolean; isGif?: boolean; isGroupMsg: boolean; isMedia: boolean; isMMS?: boolean; isNewMsg?: boolean; isNotification: boolean; isPSA?: boolean; isQuotedMsgAvailable: boolean; labels?: string[]; mentionedJidList?: string[]; notifyName?: string; parentMsgKey?: string; quotedMsg?: any; quotedMsgObj?: any; recvFresh?: boolean; self: "in" \| "out"; sender: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; senderId?: string; star?: boolean; t: number; timestamp: number; to: string; type: "chat" \| "audio" \| "ptt" \| "image" \| "video" \| "album" \| "event_creation" \| "event_response" \| "document" \| "sticker" \| "location" \| "vcard" \| "multi_vcard" \| "revoked" \| "order" \| "buttons_response" \| "list_response" \| "unknown" }): Promise` ### Usage [#usage] ```ts // Use the full media message from your message handler. const result = await client.decryptMedia(message); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); // Use the full media message received by your message handler. const result = await client.decryptMedia({ message }); ``` ```bash # Save the full incoming media message as the "message" field in request.json. # For downloadMedia, also set "path" to the destination filename. curl -X POST "http://localhost:8080/api/media/decrypt" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data-binary @request.json ``` Example response envelope: ```json { "success": true, "data": "data:image/png;base64,..." } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | DataURL | [View fields](/docs/reference/client/return-types#dataurl) | Resolves to `DataURL`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | --------------- | | Namespace | `media` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `message` | | Aliases | `media.decrypt` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | -------------- | ------ | | Primary | `POST` | `/api/media/decrypt` | `decryptMedia` | Active | | Alias | `POST` | `/api/decryptMedia` | `decryptMedia` | Active |
## `downloadMedia` [#downloadmedia] Downloads and decrypts media from a message to a file **Signature** `client.downloadMedia(message: { ack: -1 \| 0 \| 1 \| 2 \| 3 \| 4; associationType?: string; author?: string; body: string; broadcast?: boolean; caption?: string; chat: { ack?: any; contact: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; formattedTitle?: string; groupMetadata?: { announce?: boolean; creation: number; desc?: string; descOwner?: string; descTime?: number; id: string; owner?: string; participants: { id: string; isAdmin: boolean; isSuperAdmin?: boolean }[]; restrict?: boolean; subject: string }; id: string; isGroup: boolean; isReadOnly?: boolean; lastReceivedKey?: any; msgs?: any[]; muteExpiration?: number; name?: string; notSpam?: boolean; pin?: number; presence?: object; t?: number; unreadCount?: number }; chatId: string; clientUrl?: string; content: string; deprecatedMms3Url?: string; event?: { chatId: string; creatorId: string; description?: string; endTime?: number; extraGuestsAllowed?: boolean; fromMe: boolean; id: string; isCanceled: boolean; isScheduledCall?: boolean; joinLink?: string; location?: { latitude?: number \| "Infinity" \| "-Infinity" \| "NaN"; longitude?: number \| "Infinity" \| "-Infinity" \| "NaN"; name?: string }; name: string; startTime: number; timestamp: number }; expectedImageCount?: number; expectedVideoCount?: number; from: string; fromMe: boolean; id: string; invis?: boolean; isForwarded?: boolean; isGif?: boolean; isGroupMsg: boolean; isMedia: boolean; isMMS?: boolean; isNewMsg?: boolean; isNotification: boolean; isPSA?: boolean; isQuotedMsgAvailable: boolean; labels?: string[]; mentionedJidList?: string[]; notifyName?: string; parentMsgKey?: string; quotedMsg?: any; quotedMsgObj?: any; recvFresh?: boolean; self: "in" \| "out"; sender: { formattedName?: string; id: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isOnline?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; lastSeen?: number; msgs?: any[]; name?: string; profilePicThumbObj?: { eurl?: string; id?: string; img?: string; imgFull?: string; tag?: string }; pushname?: string; shortName?: string; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string }; senderId?: string; star?: boolean; t: number; timestamp: number; to: string; type: "chat" \| "audio" \| "ptt" \| "image" \| "video" \| "album" \| "event_creation" \| "event_response" \| "document" \| "sticker" \| "location" \| "vcard" \| "multi_vcard" \| "revoked" \| "order" \| "buttons_response" \| "list_response" \| "unknown" }, path: string): Promise` ### Usage [#usage-1] ```ts // Use the full media message from your message handler. const result = await client.downloadMedia(message, "./downloaded-media"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); // Use the full media message received by your message handler. const result = await client.downloadMedia({ message, path: "./downloaded-media" }); ``` ```bash # Save the full incoming media message as the "message" field in request.json. # For downloadMedia, also set "path" to the destination filename. curl -X POST "http://localhost:8080/api/media/download" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data-binary @request.json ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Path to the saved file In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Path to the saved file
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | ----------------- | | Namespace | `media` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `message`, `path` | | Aliases | `media.download` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | --------------- | ------ | | Primary | `POST` | `/api/media/download` | `downloadMedia` | Active | | Alias | `POST` | `/api/downloadMedia` | `downloadMedia` | Active |
# Messages Client API (/docs/reference/client/messages) # Messages Client API [#messages-client-api] This page documents 47 schema-registry client methods in the `messages` namespace. ## `deleteMessage` [#deletemessage] Deletes a message **Signature** `client.deleteMessage(chatId: string, messageId: string[] \| string, onlyLocal?: boolean): Promise` ### Usage [#usage] ```ts const result = await client.deleteMessage("447123456789@c.us", ["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"]); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.deleteMessage({ chatId: "447123456789@c.us", messageId: ["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"], }); ``` ```bash curl -X DELETE "http://localhost:8080/api/messages/delete" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us", "messageId": [ "false_447123456789@c.us_ABC123", "false_447123456789@c.us_DEF456" ] }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`: `true` means the runtime accepted the operation and `false` means it did not complete. Provider-side revoke can still be rejected, so the value is not proof that every recipient removed the message. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | ---------------------------------- | | Namespace | `messages` | | Action | `delete` | | Functionality | `both` | | Positional parameter order | `chatId`, `messageId`, `onlyLocal` | | Aliases | `messages.delete` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | -------- | ---------------------- | --------------- | ------ | | Primary | `DELETE` | `/api/messages/delete` | `deleteMessage` | Active | | Alias | `DELETE` | `/api/deleteMessage` | `deleteMessage` | Active |
## `editMessage` [#editmessage] Edits the text of a message through the installed private patch **Signature** `client.editMessage(messageId: string, newContent: string): Promise` ### Usage [#usage-1] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.editMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", newContent: "newContent", }); ``` ```bash curl -X PUT "http://localhost:8080/api/messages/edit" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "newContent": "newContent" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | ------------------------- | | Namespace | `messages` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `messageId`, `newContent` | | Aliases | `messages.edit` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | ------------- | ------ | | Primary | `PUT` | `/api/messages/edit` | `editMessage` | Active | | Alias | `PUT` | `/api/editMessage` | `editMessage` | Active |
## `forwardMessages` [#forwardmessages] Forwards messages to a chat **Signature** `client.forwardMessages(to: string, messages: string[] \| string, skipMyMessages?: boolean): Promise` ### Usage [#usage-2] ```ts const result = await client.forwardMessages("447123456789@c.us", ["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"]); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.forwardMessages({ to: "447123456789@c.us", messages: ["false_447123456789@c.us_ABC123","false_447123456789@c.us_DEF456"], }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/forward" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "messages": [ "false_447123456789@c.us_ABC123", "false_447123456789@c.us_DEF456" ] }' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-2] ### Returns [#returns-2] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn[] \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | ---------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `messages`, `skipMyMessages` | | Aliases | `messages.forward` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ----------------- | ------ | | Primary | `POST` | `/api/messages/forward` | `forwardMessages` | Active | | Alias | `POST` | `/api/forwardMessages` | `forwardMessages` | Active |
## `getAlbumMessages` [#getalbummessages] Returns currently loaded image and video messages associated with an album, including GIF videos. The album can arrive before its media, so the array may be incomplete. Returns false if the album is not loaded or the ID belongs to another message type. **Signature** `client.getAlbumMessages(messageId: string): Promise` ### Usage [#usage-3] ```ts const result = await client.getAlbumMessages("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAlbumMessages({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -G "http://localhost:8080/api/messages/getAlbum" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-3] ### Returns [#returns-3] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Message | [View fields](/docs/reference/client/return-types#message) | A snapshot of currently loaded album media. An empty or partial array does not mean that the sender sent an empty or partial album. Each item retains its caption and can be passed to decryptMedia or downloadMedia. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. A snapshot of currently loaded album media. An empty or partial array does not mean that the sender sent an empty or partial album. Each item retains its caption and can be passed to decryptMedia or downloadMedia.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | ------------------- | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.getAlbum` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ------------------ | ------ | | Primary | `GET` | `/api/messages/getAlbum` | `getAlbumMessages` | Active | | Alias | `GET` | `/api/getAlbumMessages` | `getAlbumMessages` | Active |
## `getAllMessages` [#getallmessages] Retrieves all messages in the session **Signature** `client.getAllMessages(chatId?: string, includeMe?: boolean, includeNotifications?: boolean): Promise` ### Usage [#usage-4] ```ts const result = await client.getAllMessages(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getAllMessages(); ``` ```bash curl -X GET "http://localhost:8080/api/messages/getAll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-4] ### Returns [#returns-4] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Message | [View fields](/docs/reference/client/return-types#message) | Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ------------------------------------------------------------------ | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId`, `includeMe`, `includeNotifications` | | Aliases | `listMessages`, `messages.all`, `messages.getAll`, `messages.list` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------- | ---------------- | ------ | | Primary | `GET` | `/api/messages/getAll` | `getAllMessages` | Active | | Alias | `GET` | `/api/getAllMessages` | `getAllMessages` | Active | | Alias | `GET` | `/api/listMessages` | `listMessages` | Active | | Alias | `GET` | `/api/messages/all` | `messages.all` | Active | | Alias | `GET` | `/api/messages/list` | `messages.list` | Active |
## `getMessageById` [#getmessagebyid] Retrieves a specific message by ID **Signature** `client.getMessageById(messageId: string): Promise` ### Usage [#usage-5] ```ts const result = await client.getMessageById("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMessageById({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -G "http://localhost:8080/api/messages/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Example response envelope: ```json { "success": true, "data": { "id": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "body": "example", "type": "chat", "t": 0, "from": "447123456789@c.us", "to": "447123456789@c.us", "self": "in", "ack": "ACK_PENDING", "sender": { "id": "447123456789@c.us" }, "timestamp": 0, "content": "example", "isGroupMsg": true, "isMedia": true, "isNotification": true, "fromMe": true, "chat": { "id": "447123456789@c.us", "isGroup": true, "contact": { "id": "447123456789@c.us" } }, "chatId": "447123456789@c.us", "isQuotedMsgAvailable": true } } ``` ### Parameters [#parameters-5] ### Returns [#returns-5] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Message | [View fields](/docs/reference/client/return-types#message) | Resolves to `Message \| null \| false`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | ------------------------------------------------ | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `getMessage`, `messages.get`, `messages.getById` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ------------------ | ------ | | Primary | `GET` | `/api/messages/get` | `getMessageById` | Active | | Alias | `GET` | `/api/getMessage` | `getMessage` | Active | | Alias | `GET` | `/api/getMessageById` | `getMessageById` | Active | | Alias | `GET` | `/api/messages/getById` | `messages.getById` | Active |
## `getMessageInfo` [#getmessageinfo] Get message delivery info **Signature** `client.getMessageInfo(messageId: string): Promise` ### Usage [#usage-6] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMessageInfo({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -G "http://localhost:8080/api/messages/getInfo" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Example response envelope: ```json { "success": true, "data": { "id": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "deliveryRemaining": 0, "playedRemaining": 0, "readRemaining": 0, "delivery": [], "read": [], "played": [] } } ``` ### Parameters [#parameters-6] ### Returns [#returns-6] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | MessageInfo | [View fields](/docs/reference/client/return-types#messageinfo) | Resolves to `MessageInfo`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-6] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.getInfo` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-6] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ---------------- | ------ | | Primary | `GET` | `/api/messages/getInfo` | `getMessageInfo` | Active | | Alias | `GET` | `/api/getMessageInfo` | `getMessageInfo` | Active |
## `getMessagesForLLM` [#getmessagesforllm] Get messages formatted for LLMs **Signature** `client.getMessagesForLLM(chatId: string, last?: number): Promise` ### Usage [#usage-7] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMessagesForLLM({ chatId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/messages/getForLLM" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-7] ### Returns [#returns-7] | Prop | Value | | ----------- | ------------------------------------------------------------- | | Return type | `Promise` | | LLMMessage | [View fields](/docs/reference/client/return-types#llmmessage) | Resolves to `LLMMessage[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-7] | Prop | Value | | -------------------------- | ----------------------------------------------------------- | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId`, `last` | | Aliases | `getGptArray`, `messages.getForLLM`, `messages.getGptArray` | | Deprecated aliases | `getGptArray`, `messages.getGptArray` | | License | `none` | | WAPI override | `getGptArray` | | Deprecated | No | ### Routes [#routes-7] | Type | Method | Path | Name | Status | | ---------------- | ------ | --------------------------- | ---------------------- | -------------- | | Primary | `GET` | `/api/messages/getForLLM` | `getMessagesForLLM` | Active | | Deprecated alias | `GET` | `/api/getGptArray` | `getGptArray` | **Deprecated** | | Alias | `GET` | `/api/getMessagesForLLM` | `getMessagesForLLM` | Active | | Deprecated alias | `GET` | `/api/messages/getGptArray` | `messages.getGptArray` | **Deprecated** |
## `getMyLastMessage` [#getmylastmessage] Get last message sent by host **Signature** `client.getMyLastMessage(chatId?: string): Promise` ### Usage [#usage-8] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMyLastMessage(); ``` ```bash curl -X GET "http://localhost:8080/api/messages/getMyLast" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": { "id": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "body": "example", "type": "chat", "t": 0, "from": "447123456789@c.us", "to": "447123456789@c.us", "self": "in", "ack": "ACK_PENDING", "sender": { "id": "447123456789@c.us" }, "timestamp": 0, "content": "example", "isGroupMsg": true, "isMedia": true, "isNotification": true, "fromMe": true, "chat": { "id": "447123456789@c.us", "isGroup": true, "contact": { "id": "447123456789@c.us" } }, "chatId": "447123456789@c.us", "isQuotedMsgAvailable": true } } ``` ### Parameters [#parameters-8] ### Returns [#returns-8] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Message | [View fields](/docs/reference/client/return-types#message) | Resolves to `Message \| undefined`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-8] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `messages.getMyLast` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-8] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ------------------ | ------ | | Primary | `GET` | `/api/messages/getMyLast` | `getMyLastMessage` | Active | | Alias | `GET` | `/api/getMyLastMessage` | `getMyLastMessage` | Active |
## `getPollResults` [#getpollresults] Hydrates the local poll vote store and returns synced counts, choices and votes. Hidden-voter polls omit voter identities and the votes array. Unsynced votes may be absent. **Signature** `client.getPollResults(messageId: string): Promise` ### Usage [#usage-9] ```ts const result = await client.getPollResults("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getPollResults({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/getPollResults" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": { "pollId": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "chatId": "447123456789@c.us", "name": "example", "selectableCount": 0, "isClosed": true, "hideVoterNames": true, "options": [], "voterCount": 0, "scope": "synced" } } ``` ### Parameters [#parameters-9] ### Returns [#returns-9] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | PollResults | [View fields](/docs/reference/client/return-types#pollresults) | Resolves to `PollResults`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-9] | Prop | Value | | -------------------------- | ------------------------- | | Namespace | `messages` | | Action | `get` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.getPollResults` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-9] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------ | ---------------- | ------ | | Primary | `POST` | `/api/messages/getPollResults` | `getPollResults` | Active | | Alias | `POST` | `/api/getPollResults` | `getPollResults` | Active |
## `getUnsentMessages` [#getunsentmessages] Get unsent/pending messages **Signature** `client.getUnsentMessages(): Promise` ### Usage [#usage-11] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getUnsentMessages(); ``` ```bash curl -X GET "http://localhost:8080/api/messages/getUnsent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-11] This method does not define input parameters. ### Returns [#returns-11] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Message | [View fields](/docs/reference/client/return-types#message) | Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-11] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `messages.getUnsent` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-11] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ------------------- | ------ | | Primary | `GET` | `/api/messages/getUnsent` | `getUnsentMessages` | Active | | Alias | `GET` | `/api/getUnsentMessages` | `getUnsentMessages` | Active |
## `getVCards` [#getvcards] Extract vCards from message **Signature** `client.getVCards(messageId: string): Promise` ### Usage [#usage-12] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getVCards({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -G "http://localhost:8080/api/messages/getVCards" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "messageId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-12] ### Returns [#returns-12] | Prop | Value | | ----------- | ------------------- | | Return type | `Promise` | Resolves to `string[]`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-12] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.getVCards` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-12] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ----------- | ------ | | Primary | `GET` | `/api/messages/getVCards` | `getVCards` | Active | | Alias | `GET` | `/api/getVCards` | `getVCards` | Active |
## `loadEarlierMessages` [#loadearliermessages] Loads earlier messages from a chat **Signature** `client.loadEarlierMessages(chatId: string, count?: number, includeMe?: boolean): Promise` ### Usage [#usage-13] ```ts const result = await client.loadEarlierMessages("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.loadEarlierMessages({ chatId: "447123456789@c.us", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/loadEarlier" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-13] ### Returns [#returns-13] | Prop | Value | | ----------- | ---------------------------------------------------------- | | Return type | `Promise` | | Message | [View fields](/docs/reference/client/return-types#message) | Resolves to `Message[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-13] | Prop | Value | | -------------------------- | ----------------------------------------------------------------- | | Namespace | `messages` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId`, `count`, `includeMe` | | Aliases | `loadOlderMessages`, `messages.loadEarlier`, `messages.loadOlder` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-13] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------- | --------------------- | ------ | | Primary | `POST` | `/api/messages/loadEarlier` | `loadEarlierMessages` | Active | | Alias | `POST` | `/api/loadEarlierMessages` | `loadEarlierMessages` | Active | | Alias | `POST` | `/api/loadOlderMessages` | `loadOlderMessages` | Active | | Alias | `POST` | `/api/messages/loadOlder` | `messages.loadOlder` | Active |
## `pinMessage` [#pinmessage] Pins a message in its chat for everyone, subject to native permissions. This differs from pinChat, which changes the current account chat list. **Signature** `client.pinMessage(messageId: string, duration?: "OneDay" \| "SevenDays" \| "ThirtyDays"): Promise` ### Usage [#usage-14] ```ts const result = await client.pinMessage("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.pinMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/pin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-14] ### Returns [#returns-14] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-14] | Prop | Value | | -------------------------- | ----------------------- | | Namespace | `messages` | | Action | `set` | | Functionality | `both` | | Positional parameter order | `messageId`, `duration` | | Aliases | `messages.pin` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-14] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------- | ------------ | ------ | | Primary | `POST` | `/api/messages/pin` | `pinMessage` | Active | | Alias | `POST` | `/api/pinMessage` | `pinMessage` | Active |
## `react` [#react] React to message with emoji **Signature** `client.react(messageId: string, emoji: string): Promise` ### Usage [#usage-15] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.react({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", emoji: "emoji", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/react" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "emoji": "emoji" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-15] ### Returns [#returns-15] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-15] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `messageId`, `emoji` | | Aliases | `messages.react` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-15] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | ------- | ------ | | Primary | `POST` | `/api/messages/react` | `react` | Active | | Alias | `POST` | `/api/react` | `react` | Active |
## `sendAlbum` [#sendalbum] Sends images and videos together as a native WhatsApp album with one caption on its first item. GIFs use MP4 video data with isGif: true. Documents are rejected. Phone-number IDs are resolved to LIDs; the destination chat must already be loaded and allow sending. WhatsApp controls the minimum item count and upload limits. Runtime failures include the parent and submitted child IDs when available; inspect the chat before retrying. **Signature** `client.sendAlbum(to: string, media: { file: string; filename: string; isGif?: boolean }[], caption?: string, quotedMsgId?: string): Promise` ### Usage [#usage-17] ```ts import { readFile } from 'node:fs/promises'; // Put these real image files in your process working directory. const media = await Promise.all(['photo-1.jpg', 'photo-2.jpg', 'photo-3.jpg', 'photo-4.jpg'].map(async filename => ({ filename, file: `data:image/jpeg;base64,${(await readFile(filename)).toString('base64')}`, }))); const result = await client.sendAlbum('447123456789@c.us', media, 'One caption for the album'); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); import { readFile } from 'node:fs/promises'; // Put these real image files in your process working directory. const media = await Promise.all(['photo-1.jpg', 'photo-2.jpg', 'photo-3.jpg', 'photo-4.jpg'].map(async filename => ({ filename, file: `data:image/jpeg;base64,${(await readFile(filename)).toString('base64')}`, }))); const result = await client.sendAlbum({ to: '447123456789@c.us', media, caption: 'One caption for the album', }); ``` ```bash # Save an album request as album.json with "to", "caption", and "media". # Each media item needs "filename" and "file" containing a real image/video data URL. # See /docs/guides/messages#albums for encoding real files. curl -X POST "http://localhost:8080/api/messages/sendAlbum" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data-binary @album.json ``` Example response envelope: ```json { "success": true, "data": { "albumId": "true_447123456789@c.us_ALBUM123", "messageIds": [ "true_447123456789@c.us_PHOTO1", "true_447123456789@c.us_PHOTO2", "true_447123456789@c.us_PHOTO3", "true_447123456789@c.us_PHOTO4" ] } } ``` ### Parameters [#parameters-17] ### Returns [#returns-17] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | AlbumSendResult | [View fields](/docs/reference/client/return-types#albumsendresult) | Submitted album parent and child message IDs, in input order. Submission does not confirm recipient delivery. Runtime failures include the album ID and submitted child IDs when available. A lost connection can leave the send outcome unknown; inspect the chat before retrying. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Submitted album parent and child message IDs, in input order. Submission does not confirm recipient delivery. Runtime failures include the album ID and submitted child IDs when available. A lost connection can leave the send outcome unknown; inspect the chat before retrying.
Advanced method metadata ### Overview [#overview-17] | Prop | Value | | -------------------------- | --------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `media`, `caption`, `quotedMsgId` | | Aliases | `messages.sendAlbum` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-17] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ----------- | ------ | | Primary | `POST` | `/api/messages/sendAlbum` | `sendAlbum` | Active | | Alias | `POST` | `/api/sendAlbum` | `sendAlbum` | Active |
## `sendAudio` [#sendaudio] Sends an audio attachment with an optional quoted message. Use sendPtt for voice notes and view-once audio. **Signature** `client.sendAudio(to: string, file: string \| Buffer, filename?: string, options?: { quotedMsgId?: string }): Promise` ### Usage [#usage-18] ```ts const result = await client.sendAudio("447123456789@c.us", "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..."); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendAudio({ to: "447123456789@c.us", file: "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA...", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendAudio" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..." }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-18] ### Returns [#returns-18] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-18] | Prop | Value | | -------------------------- | ----------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `file`, `filename`, `options` | | Aliases | `messages.sendAudio` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-18] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ----------- | ------ | | Primary | `POST` | `/api/messages/sendAudio` | `sendAudio` | Active | | Alias | `POST` | `/api/sendAudio` | `sendAudio` | Active |
## `sendBanner` [#sendbanner] Sends a banner image **Signature** `client.sendBanner(to: string, base64: string): Promise` ### Usage [#usage-19] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendBanner({ to: "447123456789@c.us", base64: "base64", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendBanner" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "base64": "base64" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-19] ### Returns [#returns-19] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-19] | Prop | Value | | -------------------------- | --------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `base64` | | Aliases | `messages.sendBanner` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-19] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | ------------ | ------ | | Primary | `POST` | `/api/messages/sendBanner` | `sendBanner` | Active | | Alias | `POST` | `/api/sendBanner` | `sendBanner` | Active |
## `sendContact` [#sendcontact] Sends a single contact **Signature** `client.sendContact(to: string, contactId: string \| string[]): Promise` ### Usage [#usage-21] ```ts const result = await client.sendContact("447123456789@c.us", "447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendContact({ to: "447123456789@c.us", contactId: "447123456789@c.us", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendContact" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "contactId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-21] ### Returns [#returns-21] | Prop | Value | | --------------- | ----------------------------------------------------------------------- | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean \| (MessageIdReturn \| boolean)[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-21] | Prop | Value | | -------------------------- | ---------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `contactId` | | Aliases | `messages.sendContact` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-21] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------- | ------------- | ------ | | Primary | `POST` | `/api/messages/sendContact` | `sendContact` | Active | | Alias | `POST` | `/api/sendContact` | `sendContact` | Active |
## `sendFile` [#sendfile] Sends an image, video, audio file or document using MIME detection or an explicit typed presentation. Incompatible options are rejected before sending. Media is not transcoded. **Signature** `client.sendFile(to: string, file: string \| Buffer, filename?: string, options?: { caption?: string; quotedMsgId?: string; type?: "auto"; viewOnce?: boolean } \| { caption?: string; quotedMsgId?: string; type: "image"; viewOnce?: boolean } \| { caption?: string; isGif?: boolean; quotedMsgId?: string; type: "video"; videoNote?: boolean; viewOnce?: boolean } \| { ptt?: boolean; quotedMsgId?: string; type: "audio"; viewOnce?: boolean } \| { caption?: string; quotedMsgId?: string; type: "document" }): Promise` ### Usage [#usage-22] ```ts const result = await client.sendFile("447123456789@c.us", "data:application/pdf;base64,JVBERi0xLjQK..."); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendFile({ to: "447123456789@c.us", file: "data:application/pdf;base64,JVBERi0xLjQK...", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendFile" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:application/pdf;base64,JVBERi0xLjQK..." }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-22] ### Returns [#returns-22] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-22] | Prop | Value | | -------------------------- | ----------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `file`, `filename`, `options` | | Aliases | `messages.sendFile` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-22] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ---------- | ------ | | Primary | `POST` | `/api/messages/sendFile` | `sendFile` | Active | | Alias | `POST` | `/api/sendFile` | `sendFile` | Active |
## `sendFileFromUrl` [#sendfilefromurl] Downloads media and sends it through sendFile with the same typed presentation options. **Signature** `client.sendFileFromUrl(to: string, url: string, filename?: string, options?: { caption?: string; quotedMsgId?: string; type?: "auto"; viewOnce?: boolean } \| { caption?: string; quotedMsgId?: string; type: "image"; viewOnce?: boolean } \| { caption?: string; isGif?: boolean; quotedMsgId?: string; type: "video"; videoNote?: boolean; viewOnce?: boolean } \| { ptt?: boolean; quotedMsgId?: string; type: "audio"; viewOnce?: boolean } \| { caption?: string; quotedMsgId?: string; type: "document" }, headers?: object): Promise` ### Usage [#usage-23] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendFileFromUrl({ to: "447123456789@c.us", url: "url", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendFileFromUrl" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "url": "url" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-23] ### Returns [#returns-23] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Message ID or success status This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Message ID or success status
Advanced method metadata ### Overview [#overview-23] | Prop | Value | | -------------------------- | --------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `url`, `filename`, `options`, `headers` | | Aliases | `messages.sendFileFromUrl` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-23] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ----------------- | ------ | | Primary | `POST` | `/api/messages/sendFileFromUrl` | `sendFileFromUrl` | Active | | Alias | `POST` | `/api/sendFileFromUrl` | `sendFileFromUrl` | Active |
## `sendImage` [#sendimage] Sends media through the native attachment pipeline, including images and videos. Options select presentation, view once, GIF playback or circular video notes; media is not transcoded. Returns submission status, not a delivery receipt. **Signature** `client.sendImage(to: string, file: string \| Buffer, filename?: string, options?: { caption?: string; quotedMsgId?: string; type?: "auto"; viewOnce?: boolean } \| { caption?: string; quotedMsgId?: string; type: "image"; viewOnce?: boolean } \| { caption?: string; isGif?: boolean; quotedMsgId?: string; type: "video"; videoNote?: boolean; viewOnce?: boolean } \| { ptt?: boolean; quotedMsgId?: string; type: "audio"; viewOnce?: boolean } \| { caption?: string; quotedMsgId?: string; type: "document" }): Promise` ### Usage [#usage-24] ```ts const result = await client.sendImage("447123456789@c.us", "data:application/pdf;base64,JVBERi0xLjQK..."); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendImage({ to: "447123456789@c.us", file: "data:application/pdf;base64,JVBERi0xLjQK...", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendImage" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:application/pdf;base64,JVBERi0xLjQK..." }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-24] ### Returns [#returns-24] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-24] | Prop | Value | | -------------------------- | ----------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `file`, `filename`, `options` | | Aliases | `messages.sendImage` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-24] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ----------- | ------ | | Primary | `POST` | `/api/messages/sendImage` | `sendImage` | Active | | Alias | `POST` | `/api/sendImage` | `sendImage` | Active |
## `sendLinkWithAutoPreview` [#sendlinkwithautopreview] Sends a link with auto-generated preview **Signature** `client.sendLinkWithAutoPreview(to: string, url: string, text?: string, thumbnail?: string, quotedMsgId?: string, customSize?: { height: number; width: number }): Promise` ### Usage [#usage-26] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendLinkWithAutoPreview({ to: "447123456789@c.us", url: "url", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendLinkWithAutoPreview" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "url": "url" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-26] ### Returns [#returns-26] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-26] | Prop | Value | | -------------------------- | ------------------------------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `url`, `text`, `thumbnail`, `quotedMsgId`, `customSize` | | Aliases | `messages.sendLink`, `messages.sendLinkWithAutoPreview`, `sendLink` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-26] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------------------- | ------------------------- | ------ | | Primary | `POST` | `/api/messages/sendLinkWithAutoPreview` | `sendLinkWithAutoPreview` | Active | | Alias | `POST` | `/api/messages/sendLink` | `messages.sendLink` | Active | | Alias | `POST` | `/api/sendLink` | `sendLink` | Active | | Alias | `POST` | `/api/sendLinkWithAutoPreview` | `sendLinkWithAutoPreview` | Active |
## `sendLocation` [#sendlocation] Sends a location to a chat **Signature** `client.sendLocation(to: string, lat: number \| string, lng: number \| string, loc?: string, address?: string, url?: string): Promise` ### Usage [#usage-28] ```ts const result = await client.sendLocation("447123456789@c.us", 51.5074, -0.1278); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendLocation({ to: "447123456789@c.us", lat: 51.5074, lng: -0.1278, }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendLocation" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "lat": 51.5074, "lng": -0.1278 }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-28] ### Returns [#returns-28] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-28] | Prop | Value | | -------------------------- | ------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `lat`, `lng`, `loc`, `address`, `url` | | Aliases | `messages.sendLocation` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-28] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | -------------- | ------ | | Primary | `POST` | `/api/messages/sendLocation` | `sendLocation` | Active | | Alias | `POST` | `/api/sendLocation` | `sendLocation` | Active |
## `sendMessageWithThumb` [#sendmessagewiththumb] Sends a link preview message with custom thumbnail **Signature** `client.sendMessageWithThumb(thumb: string, url: string, title: string, description: string, content: string, to: string, quotedMsgId?: string, customSize?: { height: number; width: number }): Promise` ### Usage [#usage-29] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendMessageWithThumb({ thumb: "thumb", url: "url", title: "title", description: "description", content: "Hello, world!", to: "447123456789@c.us", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendLinkWithThumbnail" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "thumb": "thumb", "url": "url", "title": "title", "description": "description", "content": "Hello, world!", "to": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-29] ### Returns [#returns-29] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-29] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------------------ | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `thumb`, `url`, `title`, `description`, `content`, `to`, `quotedMsgId`, `customSize` | | Aliases | `messages.sendLinkWithThumbnail`, `messages.sendWithThumb`, `sendLinkWithThumbnail` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-29] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------------- | ------------------------ | ------ | | Primary | `POST` | `/api/messages/sendLinkWithThumbnail` | `sendMessageWithThumb` | Active | | Alias | `POST` | `/api/messages/sendWithThumb` | `messages.sendWithThumb` | Active | | Alias | `POST` | `/api/sendLinkWithThumbnail` | `sendLinkWithThumbnail` | Active | | Alias | `POST` | `/api/sendMessageWithThumb` | `sendMessageWithThumb` | Active |
## `sendMultipleContacts` [#sendmultiplecontacts] Sends multiple contacts **Signature** `client.sendMultipleContacts(to: string, contactIds: string[]): Promise` ### Usage [#usage-30] ```ts const result = await client.sendMultipleContacts("447123456789@c.us", ["447123456789@c.us"]); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendMultipleContacts({ to: "447123456789@c.us", contactIds: ["447123456789@c.us"], }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendContacts" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "contactIds": [ "447123456789@c.us" ] }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-30] ### Returns [#returns-30] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-30] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------ | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `contactIds` | | Aliases | `messages.sendContacts`, `messages.sendMultipleContacts`, `sendContacts` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-30] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------------ | ------------------------------- | ------ | | Primary | `POST` | `/api/messages/sendContacts` | `sendMultipleContacts` | Active | | Alias | `POST` | `/api/messages/sendMultipleContacts` | `messages.sendMultipleContacts` | Active | | Alias | `POST` | `/api/sendContacts` | `sendContacts` | Active | | Alias | `POST` | `/api/sendMultipleContacts` | `sendMultipleContacts` | Active |
## `sendPaymentRequest` [#sendpaymentrequest] Sends a payment request message **Signature** `client.sendPaymentRequest(to: string, amount: number, currency: string, message?: string): Promise` ### Usage [#usage-31] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendPaymentRequest({ to: "447123456789@c.us", amount: 1, currency: "currency", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendPaymentRequest" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "amount": 1, "currency": "currency" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-31] ### Returns [#returns-31] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-31] | Prop | Value | | -------------------------- | ------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `amount`, `currency`, `message` | | Aliases | `messages.sendPaymentRequest` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-31] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------------- | -------------------- | ------ | | Primary | `POST` | `/api/messages/sendPaymentRequest` | `sendPaymentRequest` | Active | | Alias | `POST` | `/api/sendPaymentRequest` | `sendPaymentRequest` | Active |
## `sendPoll` [#sendpoll] Sends a text poll with a selection limit, quote, optional closing time in Unix milliseconds and hidden voter names. Closing time and hidden names require native WhatsApp support for the sending account/chat. These fields select the native V6 poll payload. **Signature** `client.sendPoll(to: string, name: string, options: string[], config?: { endTime?: number; hideVoterNames?: boolean; quotedMsgId?: string; selectableCount?: number }): Promise` ### Usage [#usage-32] ```ts const result = await client.sendPoll("447123456789@c.us", "name", ["Yes", "No"]); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendPoll({ to: "447123456789@c.us", name: "name", options: ["Yes", "No"], }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendPoll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "name": "name", "options": [ "Yes", "No" ] }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-32] ### Returns [#returns-32] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-32] | Prop | Value | | -------------------------- | --------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `name`, `options`, `config` | | Aliases | `messages.sendPoll` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-32] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ---------- | ------ | | Primary | `POST` | `/api/messages/sendPoll` | `sendPoll` | Active | | Alias | `POST` | `/api/sendPoll` | `sendPoll` | Active |
## `sendPtt` [#sendptt] Sends prepared audio as a voice note, optionally view once. It does not transcode the input; use suitable voice-note audio such as Ogg Opus. **Signature** `client.sendPtt(to: string, file: string \| Buffer, options?: { quotedMsgId?: string; viewOnce?: boolean }): Promise` ### Usage [#usage-33] ```ts const result = await client.sendPtt("447123456789@c.us", "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..."); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendPtt({ to: "447123456789@c.us", file: "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA...", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendPtt" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAA..." }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-33] ### Returns [#returns-33] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-33] | Prop | Value | | -------------------------- | ------------------------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `file`, `options` | | Aliases | `messages.sendPtt`, `messages.sendVoiceNote`, `sendVoiceNote` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-33] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------------- | ------------------------ | ------ | | Primary | `POST` | `/api/messages/sendPtt` | `sendPtt` | Active | | Alias | `POST` | `/api/messages/sendVoiceNote` | `messages.sendVoiceNote` | Active | | Alias | `POST` | `/api/sendPtt` | `sendPtt` | Active | | Alias | `POST` | `/api/sendVoiceNote` | `sendVoiceNote` | Active |
## `sendReplyWithMentions` [#sendreplywithmentions] Sends a reply to a message that includes @mentions **Signature** `client.sendReplyWithMentions(to: string, content: string, replyMessageId: string, hideTags?: boolean, mentions?: string[]): Promise` ### Usage [#usage-35] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendReplyWithMentions({ to: "447123456789@c.us", content: "Hello, world!", replyMessageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendReplyWithMentions" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": "Hello, world!", "replyMessageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-35] ### Returns [#returns-35] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-35] | Prop | Value | | -------------------------- | --------------------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `content`, `replyMessageId`, `hideTags`, `mentions` | | Aliases | `messages.sendReplyWithMentions` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-35] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------------- | ----------------------- | ------ | | Primary | `POST` | `/api/messages/sendReplyWithMentions` | `sendReplyWithMentions` | Active | | Alias | `POST` | `/api/sendReplyWithMentions` | `sendReplyWithMentions` | Active |
## `sendSeen` [#sendseen] Mark message as seen **Signature** `client.sendSeen(chatId: string): Promise` ### Usage [#usage-36] ```ts const result = await client.sendSeen("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendSeen({ chatId: "447123456789@c.us", }); ``` ```bash curl -X PUT "http://localhost:8080/api/messages/sendSeen" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "chatId": "447123456789@c.us" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-36] ### Returns [#returns-36] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-36] | Prop | Value | | -------------------------- | ------------------- | | Namespace | `messages` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `chatId` | | Aliases | `messages.sendSeen` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-36] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ---------- | ------ | | Primary | `PUT` | `/api/messages/sendSeen` | `sendSeen` | Active | | Alias | `PUT` | `/api/sendSeen` | `sendSeen` | Active |
## `sendSticker` [#sendsticker] Render a sticker locally using automatic whole-job backend selection and ordered effects, then submit through the private donor finalizer. Author and pack labels are configurable; donor app links are owned by the installed patch. Returns a submitted message ID, not a delivery receipt. Unknown send outcomes must be reconciled before retrying. **Signature** `client.sendSticker(to: string, input: string, job?: { author?: string; backend?: "auto" \| "browser" \| "standalone"; background?: string; circle?: boolean; effects?: (string \| { name: string; options?: object })[]; fit?: "contain" \| "cover"; fps?: number; loopCount?: number; pack?: string; quality?: number; quotedMsgId?: string; trim?: number[] }): Promise` ### Usage [#usage-37] ```ts const result = await client.sendSticker("447123456789@c.us", "input"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendSticker({ to: "447123456789@c.us", input: "input", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendSticker" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "input": "input" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-37] ### Returns [#returns-37] | Prop | Value | | ----------- | ------------------------------------------------------------ | | Return type | `Promise` | | MessageId | [View fields](/docs/reference/client/return-types#messageid) | Validated serialized message ID. This confirms the send returned an ID, not delivery or reading. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Validated serialized message ID. This confirms the send returned an ID, not delivery or reading.
Advanced method metadata ### Overview [#overview-37] | Prop | Value | | -------------------------- | ---------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `input`, `job` | | Aliases | `messages.sendSticker` | | Deprecated aliases | - | | License | `none` | | WAPI override | `sendStickerJob` | | Deprecated | No | ### Routes [#routes-37] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------- | ------------- | ------ | | Primary | `POST` | `/api/messages/sendSticker` | `sendSticker` | Active | | Alias | `POST` | `/api/sendSticker` | `sendSticker` | Active |
## `sendText` [#sendtext] Sends a text message and returns a validated serialized message ID. The in-process Client throws SendTextError on failure. An unknown outcome means the message may have been sent: check the chat before retrying. Starting a chat with a new number requires an applied restricted or premium license. **Signature** `client.sendText(to: string, content: string, options?: unknown): Promise` Start with `await client.sendText(to, content)`. The registry still accepts an optional `options` value for compatibility, but the current Client implementation ignores it. Use `reply(to, content, messageId)` for quoted replies and `sendTextWithMentions(to, content, hideTags, mentions)` for mentions. ### Usage [#usage-38] ```ts const messageId = await client.sendText('447123456789@c.us', 'Hello from open-wa'); console.log('Message ID:', messageId); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendText("447123456789@c.us", "Hello, world!"); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendText" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": "Hello, world!" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-38] ### Returns [#returns-38] | Prop | Value | | ----------- | ------------------------------------------------------------ | | Return type | `Promise` | | MessageId | [View fields](/docs/reference/client/return-types#messageid) | Resolves to a validated serialized `MessageId` string, not a delivery or read receipt. The in-process Client throws `SendTextError`, exported by `@open-wa/wa-automate` and `@open-wa/client`, on failure. Its `code` is `INVALID_ARGUMENT`, `LICENSE_REQUIRED`, `SEND_REJECTED`, `INVALID_SEND_RESULT`, or `SEND_FAILED`. Its `outcome` is `not_sent` for invalid arguments and known runtime rejections, or `unknown` when the runtime returns no valid ID or browser evaluation fails. An unknown outcome means the message may already have been sent: check the chat before retrying. The client never retries automatically. Starting a chat with a new number still requires an applied restricted or premium license. HTTP and remote clients expose failures through their own error envelopes. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Validated serialized message ID. This confirms the send returned an ID, not delivery or reading.
Advanced method metadata ### Overview [#overview-38] | Prop | Value | | -------------------------- | -------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `content`, `options` | | Aliases | `messages.sendText` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-38] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------ | ---------- | ------ | | Primary | `POST` | `/api/messages/sendText` | `sendText` | Active | | Alias | `POST` | `/api/sendText` | `sendText` | Active |
## `sendTextWithMentions` [#sendtextwithmentions] Sends a text message that includes @mentions **Signature** `client.sendTextWithMentions(to: string, content: string, hideTags?: boolean, mentions?: string[]): Promise` ### Usage [#usage-39] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendTextWithMentions({ to: "447123456789@c.us", content: "Hello, world!", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendTextWithMentions" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "content": "Hello, world!" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-39] ### Returns [#returns-39] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-39] | Prop | Value | | -------------------------- | --------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `content`, `hideTags`, `mentions` | | Aliases | `messages.sendTextWithMentions` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-39] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------------ | ---------------------- | ------ | | Primary | `POST` | `/api/messages/sendTextWithMentions` | `sendTextWithMentions` | Active | | Alias | `POST` | `/api/sendTextWithMentions` | `sendTextWithMentions` | Active |
## `sendVCard` [#sendvcard] Sends a contact card (vCard) **Signature** `client.sendVCard(to: string, vcard: string, contactName?: string, contactNumber?: string): Promise` ### Usage [#usage-40] ```ts const result = await client.sendVCard("447123456789@c.us", "vcard"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendVCard({ to: "447123456789@c.us", vcard: "vcard", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendVCard" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "vcard": "vcard" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-40] ### Returns [#returns-40] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-40] | Prop | Value | | -------------------------- | --------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `vcard`, `contactName`, `contactNumber` | | Aliases | `messages.sendVCard` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-40] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ----------- | ------ | | Primary | `POST` | `/api/messages/sendVCard` | `sendVCard` | Active | | Alias | `POST` | `/api/sendVCard` | `sendVCard` | Active |
## `sendVideoAsGif` [#sendvideoasgif] Sends MP4 video with WhatsApp GIF playback. GIF is a video presentation flag; this method does not convert a GIF image or transcode video. **Signature** `client.sendVideoAsGif(to: string, file: string \| Buffer, filename?: string, options?: { caption?: string; quotedMsgId?: string }): Promise` ### Usage [#usage-41] ```ts const result = await client.sendVideoAsGif("447123456789@c.us", "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21..."); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendVideoAsGif({ to: "447123456789@c.us", file: "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21...", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendVideoAsGif" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "file": "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21..." }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-41] ### Returns [#returns-41] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-41] | Prop | Value | | -------------------------- | ----------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `file`, `filename`, `options` | | Aliases | `messages.sendVideoAsGif` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-41] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------ | ---------------- | ------ | | Primary | `POST` | `/api/messages/sendVideoAsGif` | `sendVideoAsGif` | Active | | Alias | `POST` | `/api/sendVideoAsGif` | `sendVideoAsGif` | Active |
## `sendYouTubeLink` [#sendyoutubelink] Sends a YouTube link with auto-generated preview **Signature** `client.sendYouTubeLink(to: string, url: string, text?: string, thumbnail?: string, quotedMsgId?: string, customSize?: { height: number; width: number }): Promise` ### Usage [#usage-42] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendYouTubeLink({ to: "447123456789@c.us", url: "url", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/sendYouTubeLink" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "url": "url" }' ``` Example response envelope: ```json { "success": true, "data": "true_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" } ``` ### Parameters [#parameters-42] ### Returns [#returns-42] | Prop | Value | | --------------- | ------------------------------------------------------------------ | | Return type | `Promise` | | MessageIdReturn | [View fields](/docs/reference/client/return-types#messageidreturn) | Resolves to `MessageIdReturn \| boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-42] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------- | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `url`, `text`, `thumbnail`, `quotedMsgId`, `customSize` | | Aliases | `messages.sendYoutubeLink`, `messages.sendYouTubeLink`, `sendYoutubeLink` | | Deprecated aliases | `messages.sendYoutubeLink`, `sendYoutubeLink` | | License | `none` | | WAPI override | `sendYoutubeLink` | | Deprecated | No | ### Routes [#routes-42] | Type | Method | Path | Name | Status | | ---------------- | ------ | ------------------------------- | -------------------------- | -------------- | | Primary | `POST` | `/api/messages/sendYouTubeLink` | `sendYouTubeLink` | Active | | Deprecated alias | `POST` | `/api/messages/sendYoutubeLink` | `messages.sendYoutubeLink` | **Deprecated** | | Deprecated alias | `POST` | `/api/sendYoutubeLink` | `sendYoutubeLink` | **Deprecated** | | Alias | `POST` | `/api/sendYouTubeLink` | `sendYouTubeLink` | Active |
## `starMessage` [#starmessage] Star a message **Signature** `client.starMessage(messageId: string): Promise` ### Usage [#usage-43] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.starMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X PUT "http://localhost:8080/api/messages/star" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-43] ### Returns [#returns-43] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-43] | Prop | Value | | -------------------------- | --------------- | | Namespace | `messages` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.star` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-43] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | ------------- | ------ | | Primary | `PUT` | `/api/messages/star` | `starMessage` | Active | | Alias | `PUT` | `/api/starMessage` | `starMessage` | Active |
## `unpinMessage` [#unpinmessage] Removes a message pin for everyone, subject to native chat permissions. **Signature** `client.unpinMessage(messageId: string): Promise` ### Usage [#usage-44] ```ts const result = await client.unpinMessage("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unpinMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/unpin" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-44] ### Returns [#returns-44] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-44] | Prop | Value | | -------------------------- | ---------------- | | Namespace | `messages` | | Action | `set` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.unpin` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-44] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------- | -------------- | ------ | | Primary | `POST` | `/api/messages/unpin` | `unpinMessage` | Active | | Alias | `POST` | `/api/unpinMessage` | `unpinMessage` | Active |
## `unstarMessage` [#unstarmessage] Unstar a message **Signature** `client.unstarMessage(messageId: string): Promise` ### Usage [#usage-45] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.unstarMessage({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X PUT "http://localhost:8080/api/messages/unstar" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-45] ### Returns [#returns-45] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-45] | Prop | Value | | -------------------------- | ----------------- | | Namespace | `messages` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `messageId` | | Aliases | `messages.unstar` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-45] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------- | --------------- | ------ | | Primary | `PUT` | `/api/messages/unstar` | `unstarMessage` | Active | | Alias | `PUT` | `/api/unstarMessage` | `unstarMessage` | Active |
## `voteOnPoll` [#voteonpoll] Votes using option IDs returned by getPollResults; an empty array removes the current vote. The poll must be open and allow this account to vote. **Signature** `client.voteOnPoll(messageId: string, optionIds: number[]): Promise` ### Usage [#usage-46] ```ts const result = await client.voteOnPoll("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", []); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.voteOnPoll({ messageId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", optionIds: [], }); ``` ```bash curl -X POST "http://localhost:8080/api/messages/voteOnPoll" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "messageId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "optionIds": [] }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-46] ### Returns [#returns-46] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-46] | Prop | Value | | -------------------------- | ------------------------ | | Namespace | `messages` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `messageId`, `optionIds` | | Aliases | `messages.voteOnPoll` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-46] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | ------------ | ------ | | Primary | `POST` | `/api/messages/voteOnPoll` | `voteOnPoll` | Active | | Alias | `POST` | `/api/voteOnPoll` | `voteOnPoll` | Active |
# Mystatus Client API (/docs/reference/client/mystatus) # Mystatus Client API [#mystatus-client-api] This page documents 1 schema-registry client methods in the `mystatus` namespace. # Namespaced client (/docs/reference/client/namespaced-client) # Namespaced client [#namespaced-client] Namespaced aliases group related methods. For example, `groups.create` resolves to `createGroup`, and `messages.sendText` resolves to `sendText`. Both names identify the same registered method. The [compact reference](/docs/reference/client/client) starts with flat method names. Turn on **Namespaced** beside **Expand parameters** to browse by namespace. Permanent method links stay the same. The [HTTP API](/docs/getting-started/easy-api) registers both forms, such as `/api/createGroup` and `/api/groups/create`. Its examples follow the switch. The [Node.js client](/docs/client-and-integrations/socket-client) and [in-process SDK](/docs/getting-started/custom-code) use flat calls such as `client.createGroup(...)`; they do not expose `client.groups.create(...)`, so their examples keep the callable form. `BaseNamespacedClient` is a generated base class for client implementations. It extends `BaseClient` and builds namespace objects from the registered aliases. It is not the class returned by `SocketClient.connect()`. See the full alias map on [Aliases](/docs/reference/client/aliases). # Return types (/docs/reference/client/return-types) # Return types [#return-types] These are TypeScript data shapes, not class instances. Follow a type from a method to inspect its fields. `unknown` marks a provider-controlled value that needs narrowing; it does not promise an undocumented shape. ## ChatId [#chatid] Opaque conversation identifier. Use the exact value returned by a message event or chat lookup.
Type declaration ```ts type ChatId = string ```
## Chat [#chat] Serialized chat returned by chat lookups and listings. Its id is an opaque ChatId; additional WhatsApp Web fields may be present.
Type declaration ```ts type Chat = { id: ChatId; name?: string; formattedTitle?: string; isGroup: boolean; contact: Contact | null; groupMetadata?: GroupMetadata | null; presence?: Record | null; t?: number; unreadCount?: number; lastReceivedKey?: unknown; msgs?: unknown[] | null; isReadOnly?: boolean; muteExpiration?: number; notSpam?: boolean; pin?: number; ack?: unknown } ```
Related types: [ChatId](/docs/reference/client/return-types#chatid), [Contact](/docs/reference/client/return-types#contact), [GroupMetadata](/docs/reference/client/return-types#groupmetadata) | Field | Type | Required | | ----------------- | -------------------------------------------------------------------------------------------- | -------- | | `id` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `name` | `string` | No | | `formattedTitle` | `string` | No | | `isGroup` | `boolean` | Yes | | `contact` | `Contact \| null` — [Contact](/docs/reference/client/return-types#contact) | Yes | | `groupMetadata` | `GroupMetadata \| null` — [GroupMetadata](/docs/reference/client/return-types#groupmetadata) | No | | `presence` | `Record \| null` | No | | `t` | `number` | No | | `unreadCount` | `number` | No | | `lastReceivedKey` | `unknown` | No | | `msgs` | `unknown[] \| null` | No | | `isReadOnly` | `boolean` | No | | `muteExpiration` | `number` | No | | `notSpam` | `boolean` | No | | `pin` | `number` | No | | `ack` | `unknown` | No | Additional provider fields may be present; narrow them before use. ## ContactId [#contactid] Opaque WhatsApp contact identifier. Preserve the exact value returned by the API.
Type declaration ```ts type ContactId = string ```
## Contact [#contact]
Type declaration ```ts type Contact = { id: ContactId; name?: string; shortName?: string; pushname?: string; formattedName?: string; isBusiness?: boolean; isEnterprise?: boolean; isMe?: boolean; isMyContact?: boolean; isPSA?: boolean; isUser?: boolean; isWAContact?: boolean; labels?: string[]; msgs?: unknown[] | null; profilePicThumbObj?: { eurl?: string; id?: ContactId; img?: string; imgFull?: string; tag?: string }; statusMute?: boolean; type?: string; verifiedLevel?: number; verifiedName?: string; isOnline?: boolean; lastSeen?: number } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid) | Field | Type | Required | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | | `name` | `string` | No | | `shortName` | `string` | No | | `pushname` | `string` | No | | `formattedName` | `string` | No | | `isBusiness` | `boolean` | No | | `isEnterprise` | `boolean` | No | | `isMe` | `boolean` | No | | `isMyContact` | `boolean` | No | | `isPSA` | `boolean` | No | | `isUser` | `boolean` | No | | `isWAContact` | `boolean` | No | | `labels` | `string[]` | No | | `msgs` | `unknown[] \| null` | No | | `profilePicThumbObj` | `{ eurl?: string; id?: ContactId; img?: string; imgFull?: string; tag?: string }` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `statusMute` | `boolean` | No | | `type` | `string` | No | | `verifiedLevel` | `number` | No | | `verifiedName` | `string` | No | | `isOnline` | `boolean` | No | | `lastSeen` | `number` | No | Additional provider fields may be present; narrow them before use. ## DataURL [#dataurl]
Type declaration ```ts type DataURL = `data:${string};base64,${string}` ```
## GroupChatId [#groupchatid] Opaque group conversation identifier returned by a group lookup or join method.
Type declaration ```ts type GroupChatId = string ```
## GroupId [#groupid] Identifier for a WhatsApp group. Use the value returned by the API.
Type declaration ```ts type GroupId = string ```
## GroupMetadata [#groupmetadata] Serialized group metadata returned by group lookups. Additional WhatsApp Web fields may be present.
Type declaration ```ts type GroupMetadata = { id: GroupId; subject: string; creation: number; owner?: ContactId; desc?: string; descTime?: number; descOwner?: ContactId; restrict?: boolean; announce?: boolean; participants: { id: ContactId; isAdmin: boolean; isSuperAdmin?: boolean }[] } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid), [GroupId](/docs/reference/client/return-types#groupid) | Field | Type | Required | | -------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `GroupId` — [GroupId](/docs/reference/client/return-types#groupid) | Yes | | `subject` | `string` | Yes | | `creation` | `number` | Yes | | `owner` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `desc` | `string` | No | | `descTime` | `number` | No | | `descOwner` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `restrict` | `boolean` | No | | `announce` | `boolean` | No | | `participants` | `{ id: ContactId; isAdmin: boolean; isSuperAdmin?: boolean }[]` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | Additional provider fields may be present; narrow them before use. ## Id [#id]
Type declaration ```ts type Id = { server: string; user: string; _serialized: string } ```
| Field | Type | Required | | ------------- | -------- | -------- | | `server` | `string` | Yes | | `user` | `string` | Yes | | `_serialized` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## Location [#location]
Type declaration ```ts type Location = { lat: number; lng: number; name?: string; address?: string; url?: string } ```
| Field | Type | Required | | --------- | -------- | -------- | | `lat` | `number` | Yes | | `lng` | `number` | Yes | | `name` | `string` | No | | `address` | `string` | No | | `url` | `string` | No | Additional provider fields may be present; narrow them before use. ## MessageIdReturn [#messageidreturn] A message ID as a string or an object containing the serialized ID. Validate it before passing it to another method.
Type declaration ```ts type MessageIdReturn = MessageId | SerializedMessageId ```
Related types: [MessageId](/docs/reference/client/return-types#messageid), [SerializedMessageId](/docs/reference/client/return-types#serializedmessageid) ## MessageId [#messageid] Message identifier returned by a send method or message event.
Type declaration ```ts type MessageId = string ```
## Message [#message]
Type declaration ```ts type Message = { id: MessageId; body: string; type: "chat" | "audio" | "ptt" | "image" | "video" | "album" | "event_creation" | "event_response" | "document" | "sticker" | "location" | "vcard" | "multi_vcard" | "revoked" | "order" | "buttons_response" | "list_response" | "unknown"; t: number; notifyName?: string; from: ChatId; to: ChatId; self: "in" | "out"; ack: "ACK_PENDING" | "ACK_SERVER" | "ACK_DEVICE" | "ACK_READ" | "ACK_PLAYED" | -1 | "ACK_ERROR" | 0 | 1 | 2 | 3 | 4; invis?: boolean; isNewMsg?: boolean; star?: boolean; recvFresh?: boolean; broadcast?: boolean; isForwarded?: boolean; labels?: string[]; mentionedJidList?: ContactId[]; caption?: string; event?: WhatsAppEvent; expectedImageCount?: number | null; expectedVideoCount?: number | null; parentMsgKey?: MessageId | null; associationType?: string | null; isGif?: boolean; sender: Contact; timestamp: number; content: string; isGroupMsg: boolean; isMMS?: boolean; isMedia: boolean; isNotification: boolean; isPSA?: boolean; fromMe: boolean; chat: Chat; chatId: ChatId; author?: string; clientUrl?: string; deprecatedMms3Url?: string; isQuotedMsgAvailable: boolean; quotedMsg?: unknown; quotedMsgObj?: unknown; senderId?: string } ```
Related types: [ChatId](/docs/reference/client/return-types#chatid), [Chat](/docs/reference/client/return-types#chat), [ContactId](/docs/reference/client/return-types#contactid), [Contact](/docs/reference/client/return-types#contact), [MessageId](/docs/reference/client/return-types#messageid), [WhatsAppEvent](/docs/reference/client/return-types#whatsappevent) | Field | Type | Required | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | | `body` | `string` | Yes | | `type` | `"chat" \| "audio" \| "ptt" \| "image" \| "video" \| "album" \| "event_creation" \| "event_response" \| "document" \| "sticker" \| "location" \| "vcard" \| "multi_vcard" \| "revoked" \| "order" \| "buttons_response" \| "list_response" \| "unknown"` | Yes | | `t` | `number` | Yes | | `notifyName` | `string` | No | | `from` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `to` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `self` | `"in" \| "out"` | Yes | | `ack` | `"ACK_PENDING" \| "ACK_SERVER" \| "ACK_DEVICE" \| "ACK_READ" \| "ACK_PLAYED" \| -1 \| "ACK_ERROR" \| 0 \| 1 \| 2 \| 3 \| 4` | Yes | | `invis` | `boolean` | No | | `isNewMsg` | `boolean` | No | | `star` | `boolean` | No | | `recvFresh` | `boolean` | No | | `broadcast` | `boolean` | No | | `isForwarded` | `boolean` | No | | `labels` | `string[]` | No | | `mentionedJidList` | `ContactId[]` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `caption` | `string` | No | | `event` | `WhatsAppEvent` — [WhatsAppEvent](/docs/reference/client/return-types#whatsappevent) | No | | `expectedImageCount` | `number \| null` | No | | `expectedVideoCount` | `number \| null` | No | | `parentMsgKey` | `MessageId \| null` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `associationType` | `string \| null` | No | | `isGif` | `boolean` | No | | `sender` | `Contact` — [Contact](/docs/reference/client/return-types#contact) | Yes | | `timestamp` | `number` | Yes | | `content` | `string` | Yes | | `isGroupMsg` | `boolean` | Yes | | `isMMS` | `boolean` | No | | `isMedia` | `boolean` | Yes | | `isNotification` | `boolean` | Yes | | `isPSA` | `boolean` | No | | `fromMe` | `boolean` | Yes | | `chat` | `Chat` — [Chat](/docs/reference/client/return-types#chat) | Yes | | `chatId` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `author` | `string` | No | | `clientUrl` | `string` | No | | `deprecatedMms3Url` | `string` | No | | `isQuotedMsgAvailable` | `boolean` | Yes | | `quotedMsg` | `unknown` | No | | `quotedMsgObj` | `unknown` | No | | `senderId` | `string` | No | Additional provider fields may be present; narrow them before use. ## SerializedMessageId [#serializedmessageid]
Type declaration ```ts type SerializedMessageId = { _serialized: string } ```
| Field | Type | Required | | ------------- | -------- | -------- | | `_serialized` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## BusinessProfile [#businessprofile]
Type declaration ```ts type BusinessProfile = { id: ContactId; tag?: string; description?: string; categories?: { id: string; localized_display_name: string }[]; profileOptions?: { commerceExperience: "catalog" | "none" | "shop"; cartEnabled: boolean }; email?: string; website?: string[]; businessHours?: { config: Record; timezone: string }; catalogStatus?: string; address?: string; fbPage?: unknown; igProfessional?: unknown; isProfileLinked?: boolean; coverPhoto?: { id: string; url: string }; latitude?: number; longitude?: number } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid) | Field | Type | Required | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | | `tag` | `string` | No | | `description` | `string` | No | | `categories` | `{ id: string; localized_display_name: string }[]` | No | | `profileOptions` | `{ commerceExperience: "catalog" \| "none" \| "shop"; cartEnabled: boolean }` | No | | `email` | `string` | No | | `website` | `string[]` | No | | `businessHours` | `{ config: Record; timezone: string }` | No | | `catalogStatus` | `string` | No | | `address` | `string` | No | | `fbPage` | `unknown` | No | | `igProfessional` | `unknown` | No | | `isProfileLinked` | `boolean` | No | | `coverPhoto` | `{ id: string; url: string }` | No | | `latitude` | `number` | No | | `longitude` | `number` | No | Additional provider fields may be present; narrow them before use. ## CartItem [#cartitem]
Type declaration ```ts type CartItem = { id: string; name: string; qty: number; thumbnailId: string; thumbnailUrl: string } ```
| Field | Type | Required | | -------------- | -------- | -------- | | `id` | `string` | Yes | | `name` | `string` | Yes | | `qty` | `number` | Yes | | `thumbnailId` | `string` | Yes | | `thumbnailUrl` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## CommonGroup [#commongroup]
Type declaration ```ts type CommonGroup = { id: GroupId; title: string } ```
Related types: [GroupId](/docs/reference/client/return-types#groupid) | Field | Type | Required | | ------- | ------------------------------------------------------------------ | -------- | | `id` | `GroupId` — [GroupId](/docs/reference/client/return-types#groupid) | Yes | | `title` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## CommunityAdminIds [#communityadminids]
Type declaration ```ts type CommunityAdminIds = { id: GroupChatId; admins: ContactId[]; subgroup: boolean } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid), [GroupChatId](/docs/reference/client/return-types#groupchatid) | Field | Type | Required | | ---------- | ------------------------------------------------------------------------------ | -------- | | `id` | `GroupChatId` — [GroupChatId](/docs/reference/client/return-types#groupchatid) | Yes | | `admins` | `ContactId[]` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | | `subgroup` | `boolean` | Yes | Additional provider fields may be present; narrow them before use. ## CommunityAdmins [#communityadmins]
Type declaration ```ts type CommunityAdmins = { id: GroupChatId; admins: Contact[]; subgroup: boolean } ```
Related types: [Contact](/docs/reference/client/return-types#contact), [GroupChatId](/docs/reference/client/return-types#groupchatid) | Field | Type | Required | | ---------- | ------------------------------------------------------------------------------ | -------- | | `id` | `GroupChatId` — [GroupChatId](/docs/reference/client/return-types#groupchatid) | Yes | | `admins` | `Contact[]` — [Contact](/docs/reference/client/return-types#contact) | Yes | | `subgroup` | `boolean` | Yes | Additional provider fields may be present; narrow them before use. ## CommunityMetadata [#communitymetadata]
Type declaration ```ts type CommunityMetadata = { id: GroupId; subject: string; creation: number; owner?: ContactId; desc?: string; descTime?: number; descOwner?: ContactId; restrict?: boolean; announce?: boolean; participants: { id: ContactId; isAdmin: boolean; isSuperAdmin?: boolean }[]; subGroups: GroupMetadata[] } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid), [GroupId](/docs/reference/client/return-types#groupid), [GroupMetadata](/docs/reference/client/return-types#groupmetadata) | Field | Type | Required | | -------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `GroupId` — [GroupId](/docs/reference/client/return-types#groupid) | Yes | | `subject` | `string` | Yes | | `creation` | `number` | Yes | | `owner` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `desc` | `string` | No | | `descTime` | `number` | No | | `descOwner` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `restrict` | `boolean` | No | | `announce` | `boolean` | No | | `participants` | `{ id: ContactId; isAdmin: boolean; isSuperAdmin?: boolean }[]` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | | `subGroups` | `GroupMetadata[]` — [GroupMetadata](/docs/reference/client/return-types#groupmetadata) | Yes | Additional provider fields may be present; narrow them before use. ## CommunityParticipantIds [#communityparticipantids]
Type declaration ```ts type CommunityParticipantIds = { id: GroupChatId; participants: ContactId[]; subgroup: boolean } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid), [GroupChatId](/docs/reference/client/return-types#groupchatid) | Field | Type | Required | | -------------- | ------------------------------------------------------------------------------ | -------- | | `id` | `GroupChatId` — [GroupChatId](/docs/reference/client/return-types#groupchatid) | Yes | | `participants` | `ContactId[]` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | | `subgroup` | `boolean` | Yes | Additional provider fields may be present; narrow them before use. ## CommunityParticipants [#communityparticipants]
Type declaration ```ts type CommunityParticipants = { id: GroupChatId; participants: Contact[]; subgroup: boolean } ```
Related types: [Contact](/docs/reference/client/return-types#contact), [GroupChatId](/docs/reference/client/return-types#groupchatid) | Field | Type | Required | | -------------- | ------------------------------------------------------------------------------ | -------- | | `id` | `GroupChatId` — [GroupChatId](/docs/reference/client/return-types#groupchatid) | Yes | | `participants` | `Contact[]` — [Contact](/docs/reference/client/return-types#contact) | Yes | | `subgroup` | `boolean` | Yes | Additional provider fields may be present; narrow them before use. ## ContactStatus [#contactstatus]
Type declaration ```ts type ContactStatus = { id: string; status: string } ```
| Field | Type | Required | | -------- | -------- | -------- | | `id` | `string` | Yes | | `status` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## FeatureFlags [#featureflags] WhatsApp feature flags keyed by name. Values are provider-defined and must be narrowed before use.
Type declaration ```ts type FeatureFlags = Record ```
## GroupCreationResult [#groupcreationresult]
Type declaration ```ts type GroupCreationResult = { gid: GroupId | Id; status?: number; participants?: Record | Record[] } ```
Related types: [GroupId](/docs/reference/client/return-types#groupid), [Id](/docs/reference/client/return-types#id) | Field | Type | Required | | -------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | | `gid` | `GroupId \| Id` — [GroupId](/docs/reference/client/return-types#groupid), [Id](/docs/reference/client/return-types#id) | Yes | | `status` | `number` | No | | `participants` | `Record \| Record[]` | No | Additional provider fields may be present; narrow them before use. ## HealthCheck [#healthcheck] Browser health diagnostics. This is not the Easy API /health HTTP envelope.
Type declaration ```ts type HealthCheck = { queuedMessages?: number; state?: string; isPhoneDisconnected?: boolean; isHere?: boolean; wapiInjected?: boolean; online?: boolean; tryingToReachPhone?: boolean; retryingIn?: number; batteryLow?: boolean } ```
| Field | Type | Required | | --------------------- | --------- | -------- | | `queuedMessages` | `number` | No | | `state` | `string` | No | | `isPhoneDisconnected` | `boolean` | No | | `isHere` | `boolean` | No | | `wapiInjected` | `boolean` | No | | `online` | `boolean` | No | | `tryingToReachPhone` | `boolean` | No | | `retryingIn` | `number` | No | | `batteryLow` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## HostAccount [#hostaccount] Host account attributes plus the current account ID. Additional WhatsApp Web attributes vary by version.
Type declaration ```ts type HostAccount = { me: Id | ContactId } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid), [Id](/docs/reference/client/return-types#id) | Field | Type | Required | | ----- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | | `me` | `Id \| ContactId` — [ContactId](/docs/reference/client/return-types#contactid), [Id](/docs/reference/client/return-types#id) | Yes | Additional provider fields may be present; narrow them before use. ## LLMMessage [#llmmessage]
Type declaration ```ts type LLMMessage = { role: "user" | "assistant"; content: string } ```
| Field | Type | Required | | --------- | ----------------------- | -------- | | `role` | `"user" \| "assistant"` | Yes | | `content` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## Label [#label]
Type declaration ```ts type Label = { id: string; name: string; items: ({ type: "Chat" | "Contact" | "Message"; id: string })[] } ```
Related types: [Chat](/docs/reference/client/return-types#chat), [Contact](/docs/reference/client/return-types#contact), [Message](/docs/reference/client/return-types#message) | Field | Type | Required | | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `string` | Yes | | `name` | `string` | Yes | | `items` | `({ type: "Chat" \| "Contact" \| "Message"; id: string })[]` — [Chat](/docs/reference/client/return-types#chat), [Contact](/docs/reference/client/return-types#contact), [Message](/docs/reference/client/return-types#message) | Yes | Additional provider fields may be present; narrow them before use. ## LicenseType [#licensetype]
Type declaration ```ts type LicenseType = "CUSTOM" | "B2B_RESTRICTED_VOLUME_LICENSE" | "Insiders Program" | "Text Story License Key" | "Image Story License Key" | "Video Story License Key" | "Premium License Key" | "NONE" ```
Related types: [Story](/docs/reference/client/return-types#story) ## MessageInfo [#messageinfo]
Type declaration ```ts type MessageInfo = { id: MessageId; deliveryRemaining: number; playedRemaining: number; readRemaining: number; delivery: MessageInteraction[]; read: MessageInteraction[]; played: MessageInteraction[] } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid), [MessageInteraction](/docs/reference/client/return-types#messageinteraction) | Field | Type | Required | | ------------------- | ----------------------------------------------------------------------------------------------------- | -------- | | `id` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | | `deliveryRemaining` | `number` | Yes | | `playedRemaining` | `number` | Yes | | `readRemaining` | `number` | Yes | | `delivery` | `MessageInteraction[]` — [MessageInteraction](/docs/reference/client/return-types#messageinteraction) | Yes | | `read` | `MessageInteraction[]` — [MessageInteraction](/docs/reference/client/return-types#messageinteraction) | Yes | | `played` | `MessageInteraction[]` — [MessageInteraction](/docs/reference/client/return-types#messageinteraction) | Yes | Additional provider fields may be present; narrow them before use. ## MessageInteraction [#messageinteraction]
Type declaration ```ts type MessageInteraction = { id: ContactId; t: number } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid) | Field | Type | Required | | ----- | ------------------------------------------------------------------------ | -------- | | `id` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | Yes | | `t` | `number` | Yes | Additional provider fields may be present; narrow them before use. ## NumberCheck [#numbercheck]
Type declaration ```ts type NumberCheck = { id: Id | ContactId; status?: unknown; isBusiness?: boolean; canReceiveMessage: boolean; numberExists?: boolean } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid), [Id](/docs/reference/client/return-types#id) | Field | Type | Required | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `Id \| ContactId` — [ContactId](/docs/reference/client/return-types#contactid), [Id](/docs/reference/client/return-types#id) | Yes | | `status` | `unknown` | No | | `isBusiness` | `boolean` | No | | `canReceiveMessage` | `boolean` | Yes | | `numberExists` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## NumberProfile [#numberprofile] Provisional number profile based on the public Contact fields. Fields are optional and additional provider fields are unknown. The licensed getNumberProfile payload has not been verified against this model.
Type declaration ```ts type NumberProfile = { id?: ContactId; name?: string | undefined; shortName?: string | undefined; pushname?: string | undefined; formattedName?: string | undefined; isBusiness?: boolean | undefined; isEnterprise?: boolean | undefined; isMe?: boolean | undefined; isMyContact?: boolean | undefined; isPSA?: boolean | undefined; isUser?: boolean | undefined; isWAContact?: boolean | undefined; labels?: string[] | undefined; msgs?: unknown[] | null | undefined; profilePicThumbObj?: { eurl?: string; id?: ContactId; img?: string; imgFull?: string; tag?: string } | undefined; statusMute?: boolean | undefined; type?: string | undefined; verifiedLevel?: number | undefined; verifiedName?: string | undefined; isOnline?: boolean | undefined; lastSeen?: number | undefined } ```
Related types: [ContactId](/docs/reference/client/return-types#contactid) | Field | Type | Required | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `name` | `string \| undefined` | No | | `shortName` | `string \| undefined` | No | | `pushname` | `string \| undefined` | No | | `formattedName` | `string \| undefined` | No | | `isBusiness` | `boolean \| undefined` | No | | `isEnterprise` | `boolean \| undefined` | No | | `isMe` | `boolean \| undefined` | No | | `isMyContact` | `boolean \| undefined` | No | | `isPSA` | `boolean \| undefined` | No | | `isUser` | `boolean \| undefined` | No | | `isWAContact` | `boolean \| undefined` | No | | `labels` | `string[] \| undefined` | No | | `msgs` | `unknown[] \| null \| undefined` | No | | `profilePicThumbObj` | `{ eurl?: string; id?: ContactId; img?: string; imgFull?: string; tag?: string } \| undefined` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `statusMute` | `boolean \| undefined` | No | | `type` | `string \| undefined` | No | | `verifiedLevel` | `number \| undefined` | No | | `verifiedName` | `string \| undefined` | No | | `isOnline` | `boolean \| undefined` | No | | `lastSeen` | `number \| undefined` | No | Additional provider fields may be present; narrow them before use. ## Order [#order]
Type declaration ```ts type Order = { id: string; createdAt: number; currency: string; products: CartItem[]; sellerJid: string; subtotal: string; total: string; message?: Message } ```
Related types: [Message](/docs/reference/client/return-types#message), [CartItem](/docs/reference/client/return-types#cartitem) | Field | Type | Required | | ----------- | ----------------------------------------------------------------------- | -------- | | `id` | `string` | Yes | | `createdAt` | `number` | Yes | | `currency` | `string` | Yes | | `products` | `CartItem[]` — [CartItem](/docs/reference/client/return-types#cartitem) | Yes | | `sellerJid` | `string` | Yes | | `subtotal` | `string` | Yes | | `total` | `string` | Yes | | `message` | `Message` — [Message](/docs/reference/client/return-types#message) | No | Additional provider fields may be present; narrow them before use. ## ProcessStats [#processstats] Process diagnostics keyed by process ID. The provider controls each entry; no stable entry shape is guaranteed.
Type declaration ```ts type ProcessStats = Record ```
## Product [#product]
Type declaration ```ts type Product = { id: string; currency: string; name?: string; description?: string; isHidden?: boolean; catalogWid?: string; url?: string; availability?: number | "unknown"; reviewStatus?: "NO_REVIEW" | "PENDING" | "REJECTED" | "APPROVED" | "OUTDATED"; imageCdnUrl?: string; imageCount?: number; additionalImageCdnUrl?: string[]; priceAmount1000?: number; retailerId?: string; t?: number } ```
| Field | Type | Required | | ----------------------- | -------------------------------------------------------------------- | -------- | | `id` | `string` | Yes | | `currency` | `string` | Yes | | `name` | `string` | No | | `description` | `string` | No | | `isHidden` | `boolean` | No | | `catalogWid` | `string` | No | | `url` | `string` | No | | `availability` | `number \| "unknown"` | No | | `reviewStatus` | `"NO_REVIEW" \| "PENDING" \| "REJECTED" \| "APPROVED" \| "OUTDATED"` | No | | `imageCdnUrl` | `string` | No | | `imageCount` | `number` | No | | `additionalImageCdnUrl` | `string[]` | No | | `priceAmount1000` | `number` | No | | `retailerId` | `string` | No | | `t` | `number` | No | Additional provider fields may be present; narrow them before use. ## Story [#story] Message-shaped status entry, based on the legacy story API. Chat and sender may be absent or null. The licensed getStories payload has not been verified against this model.
Type declaration ```ts type Story = { id: MessageId; body: string; type: "chat" | "audio" | "ptt" | "image" | "video" | "album" | "event_creation" | "event_response" | "document" | "sticker" | "location" | "vcard" | "multi_vcard" | "revoked" | "order" | "buttons_response" | "list_response" | "unknown"; t: number; notifyName?: string; from: ChatId; to: ChatId; self: "in" | "out"; ack: "ACK_PENDING" | "ACK_SERVER" | "ACK_DEVICE" | "ACK_READ" | "ACK_PLAYED" | -1 | "ACK_ERROR" | 0 | 1 | 2 | 3 | 4; invis?: boolean; isNewMsg?: boolean; star?: boolean; recvFresh?: boolean; broadcast?: boolean; isForwarded?: boolean; labels?: string[]; mentionedJidList?: ContactId[]; caption?: string; event?: WhatsAppEvent; expectedImageCount?: number | null; expectedVideoCount?: number | null; parentMsgKey?: MessageId | null; associationType?: string | null; isGif?: boolean; timestamp: number; content: string; isGroupMsg: boolean; isMMS?: boolean; isMedia: boolean; isNotification: boolean; isPSA?: boolean; fromMe: boolean; chatId: ChatId; author?: string; clientUrl?: string; deprecatedMms3Url?: string; isQuotedMsgAvailable: boolean; quotedMsg?: unknown; quotedMsgObj?: unknown; senderId?: string; chat?: Chat | null; sender?: Contact | null } ```
Related types: [ChatId](/docs/reference/client/return-types#chatid), [Chat](/docs/reference/client/return-types#chat), [ContactId](/docs/reference/client/return-types#contactid), [Contact](/docs/reference/client/return-types#contact), [MessageId](/docs/reference/client/return-types#messageid), [WhatsAppEvent](/docs/reference/client/return-types#whatsappevent) | Field | Type | Required | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | | `body` | `string` | Yes | | `type` | `"chat" \| "audio" \| "ptt" \| "image" \| "video" \| "album" \| "event_creation" \| "event_response" \| "document" \| "sticker" \| "location" \| "vcard" \| "multi_vcard" \| "revoked" \| "order" \| "buttons_response" \| "list_response" \| "unknown"` | Yes | | `t` | `number` | Yes | | `notifyName` | `string` | No | | `from` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `to` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `self` | `"in" \| "out"` | Yes | | `ack` | `"ACK_PENDING" \| "ACK_SERVER" \| "ACK_DEVICE" \| "ACK_READ" \| "ACK_PLAYED" \| -1 \| "ACK_ERROR" \| 0 \| 1 \| 2 \| 3 \| 4` | Yes | | `invis` | `boolean` | No | | `isNewMsg` | `boolean` | No | | `star` | `boolean` | No | | `recvFresh` | `boolean` | No | | `broadcast` | `boolean` | No | | `isForwarded` | `boolean` | No | | `labels` | `string[]` | No | | `mentionedJidList` | `ContactId[]` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `caption` | `string` | No | | `event` | `WhatsAppEvent` — [WhatsAppEvent](/docs/reference/client/return-types#whatsappevent) | No | | `expectedImageCount` | `number \| null` | No | | `expectedVideoCount` | `number \| null` | No | | `parentMsgKey` | `MessageId \| null` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `associationType` | `string \| null` | No | | `isGif` | `boolean` | No | | `timestamp` | `number` | Yes | | `content` | `string` | Yes | | `isGroupMsg` | `boolean` | Yes | | `isMMS` | `boolean` | No | | `isMedia` | `boolean` | Yes | | `isNotification` | `boolean` | Yes | | `isPSA` | `boolean` | No | | `fromMe` | `boolean` | Yes | | `chatId` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `author` | `string` | No | | `clientUrl` | `string` | No | | `deprecatedMms3Url` | `string` | No | | `isQuotedMsgAvailable` | `boolean` | Yes | | `quotedMsg` | `unknown` | No | | `quotedMsgObj` | `unknown` | No | | `senderId` | `string` | No | | `chat` | `Chat \| null` — [Chat](/docs/reference/client/return-types#chat) | No | | `sender` | `Contact \| null` — [Contact](/docs/reference/client/return-types#contact) | No | Additional provider fields may be present; narrow them before use. ## BookingContent [#bookingcontent]
Type declaration ```ts type BookingContent = { version?: 1; body: string; footer?: string; type: "booking"; title?: string; buttonLabel?: string; booking: { startAt: BookingTimestamp; endAt?: BookingTimestamp; location?: string; url?: string; managementUrl?: string; phoneNumber?: string; email?: string; description?: string; labels?: { language?: string; meetingType?: string; detailsTitle?: string; addToCalendar?: string; viewOnMap?: string; manageBooking?: string } }; actions?: (LinkAction | CallAction | CopyAction)[] } ```
Related types: [BookingTimestamp](/docs/reference/client/return-types#bookingtimestamp), [CallAction](/docs/reference/client/return-types#callaction), [CopyAction](/docs/reference/client/return-types#copyaction), [LinkAction](/docs/reference/client/return-types#linkaction) | Field | Type | Required | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `version` | `1` | No | | `body` | `string` | Yes | | `footer` | `string` | No | | `type` | `"booking"` | Yes | | `title` | `string` | No | | `buttonLabel` | `string` | No | | `booking` | `{ startAt: BookingTimestamp; endAt?: BookingTimestamp; location?: string; url?: string; managementUrl?: string; phoneNumber?: string; email?: string; description?: string; labels?: { language?: string; meetingType?: string; detailsTitle?: string; addToCalendar?: string; viewOnMap?: string; manageBooking?: string } }` — [BookingTimestamp](/docs/reference/client/return-types#bookingtimestamp) | Yes | | `actions` | `(LinkAction \| CallAction \| CopyAction)[]` — [CallAction](/docs/reference/client/return-types#callaction), [CopyAction](/docs/reference/client/return-types#copyaction), [LinkAction](/docs/reference/client/return-types#linkaction) | No | Additional provider fields may be present; narrow them before use. ## BookingTimestamp [#bookingtimestamp] Validated native booking timestamp, encoded as UTC ISO 8601 with whole seconds and no fractional seconds.
Type declaration ```ts type BookingTimestamp = import('@open-wa/schema/interactive').BookingTimestamp ```
## ButtonsContent [#buttonscontent]
Type declaration ```ts type ButtonsContent = { version?: 1; body: string; footer?: string; type: "buttons"; header?: InteractiveHeader; actions: InteractiveAction[] } ```
Related types: [InteractiveAction](/docs/reference/client/return-types#interactiveaction), [InteractiveHeader](/docs/reference/client/return-types#interactiveheader) | Field | Type | Required | | --------- | -------------------------------------------------------------------------------------------------- | -------- | | `version` | `1` | No | | `body` | `string` | Yes | | `footer` | `string` | No | | `type` | `"buttons"` | Yes | | `header` | `InteractiveHeader` — [InteractiveHeader](/docs/reference/client/return-types#interactiveheader) | No | | `actions` | `InteractiveAction[]` — [InteractiveAction](/docs/reference/client/return-types#interactiveaction) | Yes | Additional provider fields may be present; narrow them before use. ## CallAction [#callaction]
Type declaration ```ts type CallAction = { type: "call"; label: string; phoneNumber: string } ```
| Field | Type | Required | | ------------- | -------- | -------- | | `type` | `"call"` | Yes | | `label` | `string` | Yes | | `phoneNumber` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## CarouselCard [#carouselcard]
Type declaration ```ts type CarouselCard = { body: string; header: { type: "image"; source: string; filename?: string; title?: string }; actions: InteractiveAction[] } ```
Related types: [InteractiveAction](/docs/reference/client/return-types#interactiveaction) | Field | Type | Required | | --------- | -------------------------------------------------------------------------------------------------- | -------- | | `body` | `string` | Yes | | `header` | `{ type: "image"; source: string; filename?: string; title?: string }` | Yes | | `actions` | `InteractiveAction[]` — [InteractiveAction](/docs/reference/client/return-types#interactiveaction) | Yes | Additional provider fields may be present; narrow them before use. ## CarouselContent [#carouselcontent]
Type declaration ```ts type CarouselContent = { version?: 1; body: string; footer?: string; type: "carousel"; cards: CarouselCard[] } ```
Related types: [CarouselCard](/docs/reference/client/return-types#carouselcard) | Field | Type | Required | | --------- | ----------------------------------------------------------------------------------- | -------- | | `version` | `1` | No | | `body` | `string` | Yes | | `footer` | `string` | No | | `type` | `"carousel"` | Yes | | `cards` | `CarouselCard[]` — [CarouselCard](/docs/reference/client/return-types#carouselcard) | Yes | Additional provider fields may be present; narrow them before use. ## CopyAction [#copyaction]
Type declaration ```ts type CopyAction = { type: "copy"; label: string; code: string } ```
| Field | Type | Required | | ------- | -------- | -------- | | `type` | `"copy"` | Yes | | `label` | `string` | Yes | | `code` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## FormContent [#formcontent]
Type declaration ```ts type FormContent = { version?: 1; body: string; footer?: string; type: "form"; title?: string; questions: FormQuestion[] } ```
Related types: [FormQuestion](/docs/reference/client/return-types#formquestion) | Field | Type | Required | | ----------- | ----------------------------------------------------------------------------------- | -------- | | `version` | `1` | No | | `body` | `string` | Yes | | `footer` | `string` | No | | `type` | `"form"` | Yes | | `title` | `string` | No | | `questions` | `FormQuestion[]` — [FormQuestion](/docs/reference/client/return-types#formquestion) | Yes | Additional provider fields may be present; narrow them before use. ## FormQuestion [#formquestion]
Type declaration ```ts type FormQuestion = { type: "singleSelect" | "multiSelect"; id: string; label: string; options: { id: string; label: string }[]; allowCustomAnswer?: boolean } ```
| Field | Type | Required | | ------------------- | --------------------------------- | -------- | | `type` | `"singleSelect" \| "multiSelect"` | Yes | | `id` | `string` | Yes | | `label` | `string` | Yes | | `options` | `{ id: string; label: string }[]` | Yes | | `allowCustomAnswer` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## FormResponse [#formresponse]
Type declaration ```ts type FormResponse = { messageId: string; chatId: string; senderId: string; originalMessageId?: string; timestamp?: number; raw: JsonObject; type: "form"; answers: Record } ```
Related types: [JsonObject](/docs/reference/client/return-types#jsonobject) | Field | Type | Required | | ------------------- | --------------------------------------------------------------------------- | -------- | | `messageId` | `string` | Yes | | `chatId` | `string` | Yes | | `senderId` | `string` | Yes | | `originalMessageId` | `string` | No | | `timestamp` | `number` | No | | `raw` | `JsonObject` — [JsonObject](/docs/reference/client/return-types#jsonobject) | Yes | | `type` | `"form"` | Yes | | `answers` | `Record` | Yes | Additional provider fields may be present; narrow them before use. ## InteractiveAction [#interactiveaction]
Type declaration ```ts type InteractiveAction = ReplyAction | LinkAction | CallAction | CopyAction ```
Related types: [CallAction](/docs/reference/client/return-types#callaction), [CopyAction](/docs/reference/client/return-types#copyaction), [LinkAction](/docs/reference/client/return-types#linkaction), [ReplyAction](/docs/reference/client/return-types#replyaction) ## InteractiveContent [#interactivecontent]
Type declaration ```ts type InteractiveContent = ButtonsContent | ListContent | FormContent | CarouselContent | BookingContent | NativeContent ```
Related types: [BookingContent](/docs/reference/client/return-types#bookingcontent), [ButtonsContent](/docs/reference/client/return-types#buttonscontent), [CarouselContent](/docs/reference/client/return-types#carouselcontent), [FormContent](/docs/reference/client/return-types#formcontent), [ListContent](/docs/reference/client/return-types#listcontent), [NativeContent](/docs/reference/client/return-types#nativecontent) ## InteractiveHeader [#interactiveheader]
Type declaration ```ts type InteractiveHeader = { type: "text"; text: string } | { type: "image"; source: string; filename?: string; title?: string } | { type: "video"; source: string; filename?: string; title?: string } | { type: "document"; source: string; filename?: string; title?: string } ```
## InteractiveResponse [#interactiveresponse]
Type declaration ```ts type InteractiveResponse = FormResponse | { messageId: string; chatId: string; senderId: string; originalMessageId?: string; timestamp?: number; raw: JsonObject; type: "reply"; id: string; label?: string } | { messageId: string; chatId: string; senderId: string; originalMessageId?: string; timestamp?: number; raw: JsonObject; type: "list"; id: string; label?: string } | { messageId: string; chatId: string; senderId: string; originalMessageId?: string; timestamp?: number; raw: JsonObject; type: "native"; name: string; parameters?: JsonValue; parseError?: string } ```
Related types: [FormResponse](/docs/reference/client/return-types#formresponse), [JsonObject](/docs/reference/client/return-types#jsonobject), [JsonValue](/docs/reference/client/return-types#jsonvalue) ## JsonObject [#jsonobject]
Type declaration ```ts type JsonObject = Record ```
Related types: [JsonValue](/docs/reference/client/return-types#jsonvalue) ## JsonValue [#jsonvalue]
Type declaration ```ts type JsonValue = null | boolean | number | string | JsonValue[] | Record ```
## LinkAction [#linkaction]
Type declaration ```ts type LinkAction = { type: "url"; label: string; url: string } ```
| Field | Type | Required | | ------- | -------- | -------- | | `type` | `"url"` | Yes | | `label` | `string` | Yes | | `url` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## ListContent [#listcontent]
Type declaration ```ts type ListContent = { version?: 1; body: string; footer?: string; type: "list"; title?: string; buttonLabel?: string; sections: { title?: string; rows: ListRow[] }[] } ```
Related types: [ListRow](/docs/reference/client/return-types#listrow) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------------------------------ | -------- | | `version` | `1` | No | | `body` | `string` | Yes | | `footer` | `string` | No | | `type` | `"list"` | Yes | | `title` | `string` | No | | `buttonLabel` | `string` | No | | `sections` | `{ title?: string; rows: ListRow[] }[]` — [ListRow](/docs/reference/client/return-types#listrow) | Yes | Additional provider fields may be present; narrow them before use. ## ListRow [#listrow]
Type declaration ```ts type ListRow = { id: string; title: string; description?: string } ```
| Field | Type | Required | | ------------- | -------- | -------- | | `id` | `string` | Yes | | `title` | `string` | Yes | | `description` | `string` | No | Additional provider fields may be present; narrow them before use. ## NativeAction [#nativeaction]
Type declaration ```ts type NativeAction = { name: string; parameters?: JsonObject } ```
Related types: [JsonObject](/docs/reference/client/return-types#jsonobject) | Field | Type | Required | | ------------ | --------------------------------------------------------------------------- | -------- | | `name` | `string` | Yes | | `parameters` | `JsonObject` — [JsonObject](/docs/reference/client/return-types#jsonobject) | No | Additional provider fields may be present; narrow them before use. ## NativeContent [#nativecontent] Advanced extension for new native action types; parameters are passed through without claiming recipient support
Type declaration ```ts type NativeContent = { version?: 1; body: string; footer?: string; type: "native"; header?: InteractiveHeader; actions: NativeAction[]; parameters?: JsonObject } ```
Related types: [InteractiveHeader](/docs/reference/client/return-types#interactiveheader), [JsonObject](/docs/reference/client/return-types#jsonobject), [NativeAction](/docs/reference/client/return-types#nativeaction) | Field | Type | Required | | ------------ | ------------------------------------------------------------------------------------------------ | -------- | | `version` | `1` | No | | `body` | `string` | Yes | | `footer` | `string` | No | | `type` | `"native"` | Yes | | `header` | `InteractiveHeader` — [InteractiveHeader](/docs/reference/client/return-types#interactiveheader) | No | | `actions` | `NativeAction[]` — [NativeAction](/docs/reference/client/return-types#nativeaction) | Yes | | `parameters` | `JsonObject` — [JsonObject](/docs/reference/client/return-types#jsonobject) | No | Additional provider fields may be present; narrow them before use. ## ReplyAction [#replyaction]
Type declaration ```ts type ReplyAction = { type: "reply"; id: string; label: string } ```
| Field | Type | Required | | ------- | --------- | -------- | | `type` | `"reply"` | Yes | | `id` | `string` | Yes | | `label` | `string` | Yes | Additional provider fields may be present; narrow them before use. ## AudioAttachmentOptions [#audioattachmentoptions]
Type declaration ```ts type AudioAttachmentOptions = { quotedMsgId?: MessageId } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | Additional provider fields may be present; narrow them before use. ## AudioSendOptions [#audiosendoptions]
Type declaration ```ts type AudioSendOptions = { type: "audio"; quotedMsgId?: MessageId; viewOnce?: boolean; ptt?: boolean } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `type` | `"audio"` | Yes | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `viewOnce` | `boolean` | No | | `ptt` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## AutoMediaSendOptions [#automediasendoptions] Detect presentation from the file MIME type; view once requires an image or video
Type declaration ```ts type AutoMediaSendOptions = { type?: "auto"; caption?: string; quotedMsgId?: MessageId; viewOnce?: boolean } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `type` | `"auto"` | No | | `caption` | `string` | No | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `viewOnce` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## DocumentSendOptions [#documentsendoptions] Force document presentation, even for an image, video or audio file
Type declaration ```ts type DocumentSendOptions = { type: "document"; caption?: string; quotedMsgId?: MessageId } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `type` | `"document"` | Yes | | `caption` | `string` | No | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | Additional provider fields may be present; narrow them before use. ## GifSendOptions [#gifsendoptions]
Type declaration ```ts type GifSendOptions = { caption?: string; quotedMsgId?: MessageId } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `caption` | `string` | No | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | Additional provider fields may be present; narrow them before use. ## ImageSendOptions [#imagesendoptions]
Type declaration ```ts type ImageSendOptions = { type: "image"; caption?: string; quotedMsgId?: MessageId; viewOnce?: boolean } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `type` | `"image"` | Yes | | `caption` | `string` | No | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `viewOnce` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## MediaSendOptions [#mediasendoptions]
Type declaration ```ts type MediaSendOptions = AutoMediaSendOptions | ImageSendOptions | VideoSendOptions | AudioSendOptions | DocumentSendOptions ```
Related types: [AudioSendOptions](/docs/reference/client/return-types#audiosendoptions), [AutoMediaSendOptions](/docs/reference/client/return-types#automediasendoptions), [DocumentSendOptions](/docs/reference/client/return-types#documentsendoptions), [ImageSendOptions](/docs/reference/client/return-types#imagesendoptions), [VideoSendOptions](/docs/reference/client/return-types#videosendoptions) ## VideoSendOptions [#videosendoptions]
Type declaration ```ts type VideoSendOptions = { type: "video"; caption?: string; quotedMsgId?: MessageId; viewOnce?: boolean; isGif?: boolean; videoNote?: boolean } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `type` | `"video"` | Yes | | `caption` | `string` | No | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `viewOnce` | `boolean` | No | | `isGif` | `boolean` | No | | `videoNote` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## VoiceNoteOptions [#voicenoteoptions]
Type declaration ```ts type VoiceNoteOptions = { quotedMsgId?: MessageId; viewOnce?: boolean } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------- | ------------------------------------------------------------------------ | -------- | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `viewOnce` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## PinDuration [#pinduration]
Type declaration ```ts type PinDuration = "OneDay" | "SevenDays" | "ThirtyDays" ```
## PollResults [#pollresults]
Type declaration ```ts type PollResults = { pollId: MessageId; chatId: ChatId; name: string; selectableCount: number; endTime?: number; isClosed: boolean; hideVoterNames: boolean; options: { id: number; name: string; count: number }[]; voterCount: number; votes?: PollVote[]; scope: "synced" } ```
Related types: [ChatId](/docs/reference/client/return-types#chatid), [MessageId](/docs/reference/client/return-types#messageid), [PollVote](/docs/reference/client/return-types#pollvote) | Field | Type | Required | | ----------------- | ------------------------------------------------------------------------ | -------- | | `pollId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | | `chatId` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `name` | `string` | Yes | | `selectableCount` | `number` | Yes | | `endTime` | `number` | No | | `isClosed` | `boolean` | Yes | | `hideVoterNames` | `boolean` | Yes | | `options` | `{ id: number; name: string; count: number }[]` | Yes | | `voterCount` | `number` | Yes | | `votes` | `PollVote[]` — [PollVote](/docs/reference/client/return-types#pollvote) | No | | `scope` | `"synced"` | Yes | Additional provider fields may be present; narrow them before use. ## PollSendOptions [#pollsendoptions]
Type declaration ```ts type PollSendOptions = { selectableCount?: number; quotedMsgId?: MessageId; endTime?: number; hideVoterNames?: boolean } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ----------------- | ------------------------------------------------------------------------ | -------- | | `selectableCount` | `number` | No | | `quotedMsgId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `endTime` | `number` | No | | `hideVoterNames` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## PollVote [#pollvote]
Type declaration ```ts type PollVote = { pollId: MessageId; chatId: ChatId; id?: MessageId; senderId?: ContactId; selectedOptionIds: number[]; timestamp: number; fromMe: boolean; hideVoterNames: boolean } ```
Related types: [ChatId](/docs/reference/client/return-types#chatid), [ContactId](/docs/reference/client/return-types#contactid), [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------------- | ------------------------------------------------------------------------ | -------- | | `pollId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | | `chatId` | `ChatId` — [ChatId](/docs/reference/client/return-types#chatid) | Yes | | `id` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | No | | `senderId` | `ContactId` — [ContactId](/docs/reference/client/return-types#contactid) | No | | `selectedOptionIds` | `number[]` | Yes | | `timestamp` | `number` | Yes | | `fromMe` | `boolean` | Yes | | `hideVoterNames` | `boolean` | Yes | Additional provider fields may be present; narrow them before use. ## AlbumMedia [#albummedia]
Type declaration ```ts type AlbumMedia = { file: string; filename: string; isGif?: boolean } ```
| Field | Type | Required | | ---------- | --------- | -------- | | `file` | `string` | Yes | | `filename` | `string` | Yes | | `isGif` | `boolean` | No | Additional provider fields may be present; narrow them before use. ## AlbumSendResult [#albumsendresult] Submitted album parent and child message IDs, in input order. Submission does not confirm recipient delivery. Runtime failures include the album ID and submitted child IDs when available. A lost connection can leave the send outcome unknown; inspect the chat before retrying.
Type declaration ```ts type AlbumSendResult = { albumId: MessageId; messageIds: MessageId[] } ```
Related types: [MessageId](/docs/reference/client/return-types#messageid) | Field | Type | Required | | ------------ | -------------------------------------------------------------------------- | -------- | | `albumId` | `MessageId` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | | `messageIds` | `MessageId[]` — [MessageId](/docs/reference/client/return-types#messageid) | Yes | Additional provider fields may be present; narrow them before use. ## EventInput [#eventinput]
Type declaration ```ts type EventInput = { name: string; startTime: number; endTime?: number; description?: string; location?: string; extraGuestsAllowed?: boolean; callType?: "voice" | "video"; isScheduledCall?: boolean; requireApproval?: boolean; quotedMsgId?: string } ```
| Field | Type | Required | | -------------------- | -------------------- | -------- | | `name` | `string` | Yes | | `startTime` | `number` | Yes | | `endTime` | `number` | No | | `description` | `string` | No | | `location` | `string` | No | | `extraGuestsAllowed` | `boolean` | No | | `callType` | `"voice" \| "video"` | No | | `isScheduledCall` | `boolean` | No | | `requireApproval` | `boolean` | No | | `quotedMsgId` | `string` | No | Additional provider fields may be present; narrow them before use. ## EventListOptions [#eventlistoptions]
Type declaration ```ts type EventListOptions = { limit?: number; before?: string } ```
| Field | Type | Required | | -------- | -------- | -------- | | `limit` | `number` | No | | `before` | `string` | No | Additional provider fields may be present; narrow them before use. ## EventPage [#eventpage]
Type declaration ```ts type EventPage = { events: ({ id: string; chatId: string; creatorId: string; name: string; startTime: number; endTime?: number | null; description?: string | null; location?: { name?: string | null; latitude?: number | "Infinity" | "-Infinity" | "NaN" | null; longitude?: number | null } | null; joinLink?: string | null; isScheduledCall?: boolean | null; extraGuestsAllowed?: boolean | null; isCanceled: boolean; fromMe: boolean; timestamp: number })[]; nextCursor: string | null } ```
| Field | Type | Required | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `events` | `({ id: string; chatId: string; creatorId: string; name: string; startTime: number; endTime?: number \| null; description?: string \| null; location?: { name?: string \| null; latitude?: number \| "Infinity" \| "-Infinity" \| "NaN" \| null; longitude?: number \| null } \| null; joinLink?: string \| null; isScheduledCall?: boolean \| null; extraGuestsAllowed?: boolean \| null; isCanceled: boolean; fromMe: boolean; timestamp: number })[]` | Yes | | `nextCursor` | `string \| null` | Yes | Additional provider fields may be present; narrow them before use. ## EventResponse [#eventresponse]
Type declaration ```ts type EventResponse = { id: string; eventId: string; chatId: string; senderId: string; response: "going" | "notGoing" | "maybe" | "unknown"; timestamp: number; fromMe: boolean } ```
| Field | Type | Required | | ----------- | ----------------------------------------------- | -------- | | `id` | `string` | Yes | | `eventId` | `string` | Yes | | `chatId` | `string` | Yes | | `senderId` | `string` | Yes | | `response` | `"going" \| "notGoing" \| "maybe" \| "unknown"` | Yes | | `timestamp` | `number` | Yes | | `fromMe` | `boolean` | Yes | Additional provider fields may be present; narrow them before use. ## EventUpdate [#eventupdate]
Type declaration ```ts type EventUpdate = { name?: string; startTime?: number; endTime?: number | null; description?: string; location?: string } ```
| Field | Type | Required | | ------------- | ---------------- | -------- | | `name` | `string` | No | | `startTime` | `number` | No | | `endTime` | `number \| null` | No | | `description` | `string` | No | | `location` | `string` | No | Additional provider fields may be present; narrow them before use. ## WhatsAppEvent [#whatsappevent]
Type declaration ```ts type WhatsAppEvent = { id: string; chatId: string; creatorId: string; name: string; startTime: number; endTime?: number; description?: string; location?: { name?: string; latitude?: number | "Infinity" | "-Infinity" | "NaN"; longitude?: number | "Infinity" | "-Infinity" | "NaN" }; joinLink?: string; isScheduledCall?: boolean; extraGuestsAllowed?: boolean; isCanceled: boolean; fromMe: boolean; timestamp: number } ```
| Field | Type | Required | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `string` | Yes | | `chatId` | `string` | Yes | | `creatorId` | `string` | Yes | | `name` | `string` | Yes | | `startTime` | `number` | Yes | | `endTime` | `number` | No | | `description` | `string` | No | | `location` | `{ name?: string; latitude?: number \| "Infinity" \| "-Infinity" \| "NaN"; longitude?: number \| "Infinity" \| "-Infinity" \| "NaN" }` | No | | `joinLink` | `string` | No | | `isScheduledCall` | `boolean` | No | | `extraGuestsAllowed` | `boolean` | No | | `isCanceled` | `boolean` | Yes | | `fromMe` | `boolean` | Yes | | `timestamp` | `number` | Yes | Additional provider fields may be present; narrow them before use. # Schema pipeline (/docs/reference/client/schema-pipeline) # Schema pipeline [#schema-pipeline] Every client method is defined once with `defineMethodV2()` and registered in `clientRegistry`. All downstream artifacts are projections of that single source of truth: ```text packages/schema/src/methods/*.ts | defineMethodV2(...) v clientRegistry.getAll() | +-> gen-client-implementation.ts --> BaseClient / BaseNamespacedClient / AliasMap +-> gen-types.ts --> generated Input/Output type aliases +-> gen-openapi.ts --> openapi.json +-> gen-client-reference-docs.ts --> these reference pages ``` The registry currently holds **154 methods** across **14 namespaces**. # Data models (/docs/reference/client/schemas) # Data models [#data-models] These are the main data models. Fields are derived from the exported types in `packages/schema/src/common-types.ts`. ### Message [#message] ### Contact [#contact] ### Chat [#chat] ### GroupMetadata [#groupmetadata] # Trace sendText through the runtime (/docs/reference/client/send-text-worked-example) # Trace sendText through the runtime [#trace-sendtext-through-the-runtime] This contributor walkthrough follows `sendText` from its schema definition to runtime dispatch. For the consumer contract, start with the [sendText method block](/docs/reference/client/messages#sendtext). ## Consumer usage [#consumer-usage] ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'docs-example' }); try { const messageId = await client.sendText('447123456789@c.us', 'Hello from open-wa'); console.log('Message ID:', messageId); } finally { await client.stop(); } ``` ## Runtime trace [#runtime-trace] `sendText` is defined in `packages/schema/src/methods/messaging.ts`: ```ts export const sendText = defineMethodV2('sendText', { meta: { description: "Sends a text message and returns a validated serialized message ID. The in-process Client throws SendTextError on failure. An unknown outcome means the message may have been sent: check the chat before retrying. Starting a chat with a new number requires an applied restricted or premium license.", namespace: "messages", httpMethod: "POST" }, parameterOrder: ["to","content","options"], // input: z.object({ to, content, options }), output: SendTextResultSchema }); ``` 1. A positional call maps by `parameterOrder`. 2. An object call resolves key aliases (`chatId` → `to`, `text` → `content`). 3. `inputSchema.parseAsync()` validates before dispatch. 4. Dispatch target is `execute('sendText', validatedParams)`. 5. See the full reference on the [messages namespace page](/docs/reference/client/messages#sendtext). # Session Client API (/docs/reference/client/session) # Session Client API [#session-client-api] This page documents 17 schema-registry client methods in the `session` namespace. ## `getBatteryLevel` [#getbatterylevel] Get phone battery level **Signature** `client.getBatteryLevel(): Promise` ### Usage [#usage] ```ts const result = await client.getBatteryLevel(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getBatteryLevel(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getBatteryLevel" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": 0 } ``` ### Parameters [#parameters] This method does not define input parameters. ### Returns [#returns] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `number`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | ------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getBatteryLevel` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------ | ----------------- | ------ | | Primary | `GET` | `/api/session/getBatteryLevel` | `getBatteryLevel` | Active | | Alias | `GET` | `/api/getBatteryLevel` | `getBatteryLevel` | Active |
## `getConnectionState` [#getconnectionstate] Get connection state **Signature** `client.getConnectionState(): Promise` ### Usage [#usage-1] ```ts const result = await client.getConnectionState(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getConnectionState(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getConnectionState" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-1] This method does not define input parameters. ### Returns [#returns-1] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | ---------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getConnectionState` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------------- | -------------------- | ------ | | Primary | `GET` | `/api/session/getConnectionState` | `getConnectionState` | Active | | Alias | `GET` | `/api/getConnectionState` | `getConnectionState` | Active |
## `getFeatures` [#getfeatures] Get enabled features **Signature** `client.getFeatures(): Promise` ### Usage [#usage-2] ```ts const result = await client.getFeatures(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getFeatures(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getFeatures" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": {} } ``` ### Parameters [#parameters-2] This method does not define input parameters. ### Returns [#returns-2] | Prop | Value | | ------------ | --------------------------------------------------------------- | | Return type | `Promise` | | FeatureFlags | [View fields](/docs/reference/client/return-types#featureflags) | WhatsApp feature flags keyed by name. Values are provider-defined and must be narrowed before use. In-process Client resolves to `Promise>`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. WhatsApp feature flags keyed by name. Values are provider-defined and must be narrowed before use.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | --------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getFeatures` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | ------------- | ------ | | Primary | `GET` | `/api/session/getFeatures` | `getFeatures` | Active | | Alias | `GET` | `/api/getFeatures` | `getFeatures` | Active |
## `getHostNumber` [#gethostnumber] Get host phone number **Signature** `client.getHostNumber(): Promise` ### Usage [#usage-3] ```ts const result = await client.getHostNumber(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getHostNumber(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getHostNumber" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-3] This method does not define input parameters. ### Returns [#returns-3] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | ----------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getHostNumber` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | --------------- | ------ | | Primary | `GET` | `/api/session/getHostNumber` | `getHostNumber` | Active | | Alias | `GET` | `/api/getHostNumber` | `getHostNumber` | Active |
## `getLicenseFeatures` [#getlicensefeatures] Get enabled and disabled licensed features from the accepted licence patch. This reports licence access; actions check runtime availability automatically. **Signature** `client.getLicenseFeatures(): Promise<{ status: "valid" \| "missing" \| "invalid" \| "expired" \| "metadata_only"; keyType: string \| null; expiresAt: number \| "Infinity" \| "-Infinity" \| "NaN" \| null; features: Record }>` ### Usage [#usage-4] ```ts const result = await client.getLicenseFeatures(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getLicenseFeatures(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getLicenseFeatures" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": { "status": "valid", "keyType": "example", "expiresAt": 0, "features": {} } } ``` ### Parameters [#parameters-4] This method does not define input parameters. ### Returns [#returns-4] | Prop | Value | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Return type | `Promise<{ status: "valid" \| "missing" \| "invalid" \| "expired" \| "metadata_only"; keyType: string \| null; expiresAt: number \| "Infinity" \| "-Infinity" \| "NaN" \| null; features: Record }>` | Resolves to `{ status: "valid" \| "missing" \| "invalid" \| "expired" \| "metadata_only"; keyType: string \| null; expiresAt: number \| "Infinity" \| "-Infinity" \| "NaN" \| null; features: Record }`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ---------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getLicenseFeatures` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------------- | -------------------- | ------ | | Primary | `GET` | `/api/session/getLicenseFeatures` | `getLicenseFeatures` | Active | | Alias | `GET` | `/api/getLicenseFeatures` | `getLicenseFeatures` | Active |
## `getLicenseType` [#getlicensetype] Get license type **Signature** `client.getLicenseType(): Promise` ### Usage [#usage-5] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getLicenseType(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getLicenseType" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": "CUSTOM" } ``` ### Parameters [#parameters-5] This method does not define input parameters. ### Returns [#returns-5] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | LicenseType | [View fields](/docs/reference/client/return-types#licensetype) | Resolves to `LicenseType \| false`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | ------------------------ | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getLicenseType` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------------- | ---------------- | ------ | | Primary | `GET` | `/api/session/getLicenseType` | `getLicenseType` | Active | | Alias | `GET` | `/api/getLicenseType` | `getLicenseType` | Active |
## `getLoadedMessageCount` [#getloadedmessagecount] Get loaded message count **Signature** `client.getLoadedMessageCount(): Promise` ### Usage [#usage-6] ```ts const result = await client.getAmountOfLoadedMessages(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getLoadedMessageCount(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getLoadedMessageCount" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": 0 } ``` ### Parameters [#parameters-6] This method does not define input parameters. ### Returns [#returns-6] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `number`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-6] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `getAmountOfLoadedMessages`, `session.getAmountOfLoadedMessages`, `session.getLoadedMessageCount` | | Deprecated aliases | `getAmountOfLoadedMessages`, `session.getAmountOfLoadedMessages` | | License | `none` | | WAPI override | `getAmountOfLoadedMessages` | | Deprecated | No | ### Routes [#routes-6] | Type | Method | Path | Name | Status | | ---------------- | ------ | ---------------------------------------- | ----------------------------------- | -------------- | | Primary | `GET` | `/api/session/getLoadedMessageCount` | `getLoadedMessageCount` | Active | | Deprecated alias | `GET` | `/api/getAmountOfLoadedMessages` | `getAmountOfLoadedMessages` | **Deprecated** | | Alias | `GET` | `/api/getLoadedMessageCount` | `getLoadedMessageCount` | Active | | Deprecated alias | `GET` | `/api/session/getAmountOfLoadedMessages` | `session.getAmountOfLoadedMessages` | **Deprecated** |
## `getMe` [#getme] Get host account info **Signature** `client.getMe(): Promise` ### Usage [#usage-7] ```ts const result = await client.getMe(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getMe(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getMe" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": { "me": { "server": "example", "user": "example", "_serialized": "example" } } } ``` ### Parameters [#parameters-7] This method does not define input parameters. ### Returns [#returns-7] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | HostAccount | [View fields](/docs/reference/client/return-types#hostaccount) | Host account attributes plus the current account ID. Additional WhatsApp Web attributes vary by version. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Host account attributes plus the current account ID. Additional WhatsApp Web attributes vary by version.
Advanced method metadata ### Overview [#overview-7] | Prop | Value | | -------------------------- | --------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getMe` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-7] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------- | ------- | ------ | | Primary | `GET` | `/api/session/getMe` | `getMe` | Active | | Alias | `GET` | `/api/getMe` | `getMe` | Active |
## `getProcessStats` [#getprocessstats] Get process statistics **Signature** `client.getProcessStats(): Promise` ### Usage [#usage-8] ```ts const result = await client.getProcessStats(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getProcessStats(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getProcessStats" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": {} } ``` ### Parameters [#parameters-8] This method does not define input parameters. ### Returns [#returns-8] | Prop | Value | | ------------ | --------------------------------------------------------------- | | Return type | `Promise` | | ProcessStats | [View fields](/docs/reference/client/return-types#processstats) | Process diagnostics keyed by process ID. The provider controls each entry; no stable entry shape is guaranteed. In-process Client resolves to `Promise>`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Process diagnostics keyed by process ID. The provider controls each entry; no stable entry shape is guaranteed.
Advanced method metadata ### Overview [#overview-8] | Prop | Value | | -------------------------- | ------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.getProcessStats` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-8] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------ | ----------------- | ------ | | Primary | `GET` | `/api/session/getProcessStats` | `getProcessStats` | Active | | Alias | `GET` | `/api/getProcessStats` | `getProcessStats` | Active |
## `getSnapshot` [#getsnapshot] Take screenshot **Signature** `client.getSnapshot(chatId?: string, width?: number, height?: number): Promise` ### Usage [#usage-9] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getSnapshot(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getSnapshot" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-9] ### Returns [#returns-9] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-9] | Prop | Value | | -------------------------- | ----------------------------------------------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId`, `width`, `height` | | Aliases | `session.getSnapshot`, `session.takeScreenshot`, `takeScreenshot` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-9] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------------- | ------------------------ | ------ | | Primary | `GET` | `/api/session/getSnapshot` | `getSnapshot` | Active | | Alias | `GET` | `/api/getSnapshot` | `getSnapshot` | Active | | Alias | `GET` | `/api/session/takeScreenshot` | `session.takeScreenshot` | Active | | Alias | `GET` | `/api/takeScreenshot` | `takeScreenshot` | Active |
## `getUserAgent` [#getuseragent] Get user agent **Signature** `client.getUserAgent(userAgent?: string): Promise` ### Usage [#usage-10] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getUserAgent(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getUserAgent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-10] ### Returns [#returns-10] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API.
Advanced method metadata ### Overview [#overview-10] | Prop | Value | | -------------------------- | -------------------------------------------------------------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `userAgent` | | Aliases | `getGeneratedUserAgent`, `session.getGeneratedUserAgent`, `session.getUserAgent` | | Deprecated aliases | `getGeneratedUserAgent`, `session.getGeneratedUserAgent` | | License | `none` | | WAPI override | `getGeneratedUserAgent` | | Deprecated | No | ### Routes [#routes-10] | Type | Method | Path | Name | Status | | ---------------- | ------ | ------------------------------------ | ------------------------------- | -------------- | | Primary | `GET` | `/api/session/getUserAgent` | `getUserAgent` | Active | | Deprecated alias | `GET` | `/api/getGeneratedUserAgent` | `getGeneratedUserAgent` | **Deprecated** | | Alias | `GET` | `/api/getUserAgent` | `getUserAgent` | Active | | Deprecated alias | `GET` | `/api/session/getGeneratedUserAgent` | `session.getGeneratedUserAgent` | **Deprecated** |
## `getWAVersion` [#getwaversion] Get WhatsApp Web version **Signature** `client.getWAVersion(): Promise` ### Usage [#usage-11] ```ts const result = await client.getWAVersion(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getWAVersion(); ``` ```bash curl -X GET "http://localhost:8080/api/session/getWAVersion" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-11] This method does not define input parameters. ### Returns [#returns-11] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Resolves to `string`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-11] | Prop | Value | | -------------------------- | -------------------------------------------------------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `getWhatsAppVersion`, `session.getWAVersion`, `session.getWhatsAppVersion` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-11] | Type | Method | Path | Name | Status | | ------- | ------ | --------------------------------- | ---------------------------- | ------ | | Primary | `GET` | `/api/session/getWAVersion` | `getWAVersion` | Active | | Alias | `GET` | `/api/getWAVersion` | `getWAVersion` | Active | | Alias | `GET` | `/api/getWhatsAppVersion` | `getWhatsAppVersion` | Active | | Alias | `GET` | `/api/session/getWhatsAppVersion` | `session.getWhatsAppVersion` | Active |
## `healthCheck` [#healthcheck] Health check **Signature** `client.healthCheck(): Promise` ### Usage [#usage-12] ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.healthCheck(); ``` ```bash curl -X GET "http://localhost:8080/api/session/healthCheck" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": {} } ``` ### Parameters [#parameters-12] This method does not define input parameters. ### Returns [#returns-12] | Prop | Value | | ----------- | -------------------------------------------------------------- | | Return type | `Promise` | | HealthCheck | [View fields](/docs/reference/client/return-types#healthcheck) | Browser health diagnostics. This is not the Easy API /health HTTP envelope. This method is not declared on the in-process Client facade. Use the remote Node.js client or HTTP API. Browser health diagnostics. This is not the Easy API /health HTTP envelope.
Advanced method metadata ### Overview [#overview-12] | Prop | Value | | -------------------------- | --------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `session.healthCheck` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-12] | Type | Method | Path | Name | Status | | ------- | ------ | -------------------------- | ------------- | ------ | | Primary | `GET` | `/api/session/healthCheck` | `healthCheck` | Active | | Alias | `GET` | `/api/healthCheck` | `healthCheck` | Active |
## `isPlugged` [#isplugged] Check if phone is charging **Signature** `client.isPlugged(): Promise` ### Usage [#usage-13] ```ts const result = await client.getIsPlugged(); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.isPlugged(); ``` ```bash curl -X GET "http://localhost:8080/api/session/isPlugged" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{}' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-13] This method does not define input parameters. ### Returns [#returns-13] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-13] | Prop | Value | | -------------------------- | ----------------------------------------------------------- | | Namespace | `session` | | Action | `read` | | Functionality | `both` | | Positional parameter order | - | | Aliases | `getIsPlugged`, `session.getIsPlugged`, `session.isPlugged` | | Deprecated aliases | `getIsPlugged`, `session.getIsPlugged` | | License | `none` | | WAPI override | `getIsPlugged` | | Deprecated | No | ### Routes [#routes-13] | Type | Method | Path | Name | Status | | ---------------- | ------ | --------------------------- | ---------------------- | -------------- | | Primary | `GET` | `/api/session/isPlugged` | `isPlugged` | Active | | Deprecated alias | `GET` | `/api/getIsPlugged` | `getIsPlugged` | **Deprecated** | | Alias | `GET` | `/api/isPlugged` | `isPlugged` | Active | | Deprecated alias | `GET` | `/api/session/getIsPlugged` | `session.getIsPlugged` | **Deprecated** |
## `setMyName` [#setmyname] Set host name **Signature** `client.setMyName(name: string): Promise` ### Usage [#usage-14] ```ts const result = await client.setMyName("OpenWA Bot"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setMyName({ name: "OpenWA Bot", }); ``` ```bash curl -X PUT "http://localhost:8080/api/session/setName" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "name": "OpenWA Bot" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-14] ### Returns [#returns-14] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-14] | Prop | Value | | -------------------------- | ---------------------------------------------------------------------------------- | | Namespace | `session` | | Action | `update` | | Functionality | `personal-only` | | Positional parameter order | `name` | | Aliases | `session.setDisplayName`, `session.setMyName`, `session.setName`, `setDisplayName` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-14] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------------- | ------------------------ | ------ | | Primary | `PUT` | `/api/session/setName` | `setMyName` | Active | | Alias | `PUT` | `/api/session/setDisplayName` | `session.setDisplayName` | Active | | Alias | `PUT` | `/api/session/setMyName` | `session.setMyName` | Active | | Alias | `PUT` | `/api/setDisplayName` | `setDisplayName` | Active | | Alias | `PUT` | `/api/setMyName` | `setMyName` | Active |
## `setMyStatus` [#setmystatus] Set host status **Signature** `client.setMyStatus(statusText: string): Promise` ### Usage [#usage-15] ```ts const result = await client.setMyStatus("Available"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.setMyStatus({ statusText: "Available", }); ``` ```bash curl -X PUT "http://localhost:8080/api/session/setStatus" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "statusText": "Available" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-15] ### Returns [#returns-15] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-15] | Prop | Value | | -------------------------- | ------------------------------------------------------------------------------------ | | Namespace | `session` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `statusText` | | Aliases | `session.setMyStatus`, `session.setStatus`, `session.setStatusText`, `setStatusText` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-15] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | ----------------------- | ------ | | Primary | `PUT` | `/api/session/setStatus` | `setMyStatus` | Active | | Alias | `PUT` | `/api/session/setMyStatus` | `session.setMyStatus` | Active | | Alias | `PUT` | `/api/session/setStatusText` | `session.setStatusText` | Active | | Alias | `PUT` | `/api/setMyStatus` | `setMyStatus` | Active | | Alias | `PUT` | `/api/setStatusText` | `setStatusText` | Active |
# Status Client API (/docs/reference/client/status) # Status Client API [#status-client-api] This page documents 6 schema-registry client methods in the `status` namespace. # Events Client API (/docs/reference/client/whatsapp-events) # Events Client API [#events-client-api] This page documents 7 schema-registry client methods in the `events` namespace. ## `cancelEvent` [#cancelevent] Cancel an event created by this account using WhatsApp’s encrypted event edit action. **Signature** `client.cancelEvent(eventId: string): Promise` ### Usage [#usage] ```ts const result = await client.cancelEvent("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.cancelEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -X POST "http://localhost:8080/api/events/cancelEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "eventId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters] ### Returns [#returns] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `events` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `eventId` | | Aliases | `events.cancelEvent` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------- | ------------- | ------ | | Primary | `POST` | `/api/events/cancelEvent` | `cancelEvent` | Active | | Alias | `POST` | `/api/cancelEvent` | `cancelEvent` | Active |
## `editEvent` [#editevent] Edit an event created by this account. Unspecified fields retain their current values; endTime: null removes the end time. **Signature** `client.editEvent(eventId: string, changes: { description?: string; endTime?: number \| null; location?: string; name?: string; startTime?: number }): Promise` ### Usage [#usage-1] ```ts const result = await client.editEvent("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", {"description":"Updated meetup details"}); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.editEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", changes: {"description":"Updated meetup details"}, }); ``` ```bash curl -X POST "http://localhost:8080/api/events/editEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "eventId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "changes": { "description": "Updated meetup details" } }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-1] ### Returns [#returns-1] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-1] | Prop | Value | | -------------------------- | -------------------- | | Namespace | `events` | | Action | `update` | | Functionality | `both` | | Positional parameter order | `eventId`, `changes` | | Aliases | `events.editEvent` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-1] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ----------- | ------ | | Primary | `POST` | `/api/events/editEvent` | `editEvent` | Active | | Alias | `POST` | `/api/editEvent` | `editEvent` | Active |
## `getEvent` [#getevent] Retrieve an event by message ID from WhatsApp local history. Returns null when absent. **Signature** `client.getEvent(eventId: string): Promise` ### Usage [#usage-2] ```ts const result = await client.getEvent("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -G "http://localhost:8080/api/events/getEvent" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "eventId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Example response envelope: ```json { "success": true, "data": { "id": "example", "chatId": "example", "creatorId": "example", "name": "example", "startTime": 0, "isCanceled": true, "fromMe": true, "timestamp": 0 } } ``` ### Parameters [#parameters-2] ### Returns [#returns-2] | Prop | Value | | ------------- | ---------------------------------------------------------------- | | Return type | `Promise` | | WhatsAppEvent | [View fields](/docs/reference/client/return-types#whatsappevent) | Resolves to `WhatsAppEvent \| null`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-2] | Prop | Value | | -------------------------- | ----------------- | | Namespace | `events` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `eventId` | | Aliases | `events.getEvent` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-2] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------- | ---------- | ------ | | Primary | `GET` | `/api/events/getEvent` | `getEvent` | Active | | Alias | `GET` | `/api/getEvent` | `getEvent` | Active |
## `getEventResponses` [#geteventresponses] Get the latest decrypted RSVP per participant, including responses persisted in WhatsApp local history. **Signature** `client.getEventResponses(eventId: string): Promise` ### Usage [#usage-3] ```ts const result = await client.getEventResponses("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getEventResponses({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", }); ``` ```bash curl -G "http://localhost:8080/api/events/getEventResponses" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "eventId=false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB" ``` Example response envelope: ```json { "success": true, "data": [] } ``` ### Parameters [#parameters-3] ### Returns [#returns-3] | Prop | Value | | ------------- | ---------------------------------------------------------------- | | Return type | `Promise` | | EventResponse | [View fields](/docs/reference/client/return-types#eventresponse) | Resolves to `EventResponse[]`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-3] | Prop | Value | | -------------------------- | -------------------------- | | Namespace | `events` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `eventId` | | Aliases | `events.getEventResponses` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-3] | Type | Method | Path | Name | Status | | ------- | ------ | ------------------------------- | ------------------- | ------ | | Primary | `GET` | `/api/events/getEventResponses` | `getEventResponses` | Active | | Alias | `GET` | `/api/getEventResponses` | `getEventResponses` | Active |
## `getEvents` [#getevents] Page through a chat’s locally synced event history, newest first. Pass nextCursor as options.before to retrieve the next page. **Signature** `client.getEvents(chatId: string, options?: { before?: string; limit?: number }): Promise` ### Usage [#usage-4] ```ts const result = await client.getEvents("447123456789@c.us"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.getEvents({ chatId: "447123456789@c.us", }); ``` ```bash curl -G "http://localhost:8080/api/events/get" \ -H "x-api-key: YOUR_API_KEY" \ --data-urlencode "chatId=447123456789@c.us" ``` Example response envelope: ```json { "success": true, "data": { "events": [], "nextCursor": "example" } } ``` ### Parameters [#parameters-4] ### Returns [#returns-4] | Prop | Value | | ----------- | ------------------------------------------------------------ | | Return type | `Promise` | | EventPage | [View fields](/docs/reference/client/return-types#eventpage) | Resolves to `EventPage`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-4] | Prop | Value | | -------------------------- | ------------------- | | Namespace | `events` | | Action | `read` | | Functionality | `both` | | Positional parameter order | `chatId`, `options` | | Aliases | `events.get` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-4] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------- | ----------- | ------ | | Primary | `GET` | `/api/events/get` | `getEvents` | Active | | Alias | `GET` | `/api/getEvents` | `getEvents` | Active |
## `respondToEvent` [#respondtoevent] RSVP through WhatsApp’s native encrypted event-response action. **Signature** `client.respondToEvent(eventId: string, response: "going" \| "notGoing" \| "maybe"): Promise` ### Usage [#usage-5] ```ts const result = await client.respondToEvent("false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "going"); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.respondToEvent({ eventId: "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", response: "going", }); ``` ```bash curl -X POST "http://localhost:8080/api/events/respondToEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "eventId": "false_447123456789@c.us_9C4D0965EA5C09D591334AB6BDB07FEB", "response": "going" }' ``` Example response envelope: ```json { "success": true, "data": true } ``` ### Parameters [#parameters-5] ### Returns [#returns-5] | Prop | Value | | ----------- | ------------------ | | Return type | `Promise` | Resolves to `boolean`. A rejected Promise indicates input validation or dispatch failure. In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property.
Advanced method metadata ### Overview [#overview-5] | Prop | Value | | -------------------------- | ----------------------- | | Namespace | `events` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `eventId`, `response` | | Aliases | `events.respondToEvent` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-5] | Type | Method | Path | Name | Status | | ------- | ------ | ---------------------------- | ---------------- | ------ | | Primary | `POST` | `/api/events/respondToEvent` | `respondToEvent` | Active | | Alias | `POST` | `/api/respondToEvent` | `respondToEvent` | Active |
## `sendEvent` [#sendevent] Send a native WhatsApp event with Unix-second times. Returns its submitted message ID; delivery is observed through ACKs. **Signature** `client.sendEvent(to: string, event: { callType?: "voice" \| "video"; description?: string; endTime?: number; extraGuestsAllowed?: boolean; isScheduledCall?: boolean; location?: string; name: string; quotedMsgId?: string; requireApproval?: boolean; startTime: number }): Promise` ### Usage [#usage-6] ```ts const result = await client.sendEvent("447123456789@c.us", {"name":"Community meetup","startTime":1893456000,"description":"Meet the community"}); ``` ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://localhost:8080', 'YOUR_API_KEY'); const result = await client.sendEvent({ to: "447123456789@c.us", event: {"name":"Community meetup","startTime":1893456000,"description":"Meet the community"}, }); ``` ```bash curl -X POST "http://localhost:8080/api/events/sendEvent" \ -H "content-type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ --data '{ "to": "447123456789@c.us", "event": { "name": "Community meetup", "startTime": 1893456000, "description": "Meet the community" } }' ``` Example response envelope: ```json { "success": true, "data": "example" } ``` ### Parameters [#parameters-6] ### Returns [#returns-6] | Prop | Value | | ----------- | ----------------- | | Return type | `Promise` | Message ID In-process Client resolves to `Promise`. The contract above is for the remote Node.js client; HTTP wraps that result in its `data` property. Message ID
Advanced method metadata ### Overview [#overview-6] | Prop | Value | | -------------------------- | ------------------ | | Namespace | `events` | | Action | `send` | | Functionality | `both` | | Positional parameter order | `to`, `event` | | Aliases | `events.sendEvent` | | Deprecated aliases | - | | License | `none` | | WAPI override | - | | Deprecated | No | ### Routes [#routes-6] | Type | Method | Path | Name | Status | | ------- | ------ | ----------------------- | ----------- | ------ | | Primary | `POST` | `/api/events/sendEvent` | `sendEvent` | Active | | Alias | `POST` | `/api/sendEvent` | `sendEvent` | Active |
# Core Client Reference (/docs/reference/core) # Core Client Reference [#core-client-reference] The core client is the primary interface for interacting with WhatsApp. Use these tables to explore the available methods, properties, and configuration options. ## create vs createClient vs Easy API vs OpenWAClient relationship [#create-vs-createclient-vs-easy-api-vs-openwaclient-relationship] Open-WA exposes a few entry points because each one owns a different layer of the runtime. | Entry point | What it is | Use it when | | -------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Easy API | The hosted HTTP API and dashboard started by the CLI or API server. | You want the fastest path to a running WhatsApp API, generated docs, webhooks, or MCP tools. | | `createClient` | The low-level core factory exported from `@open-wa/core` and re-exported by `@open-wa/wa-automate`. | You want to own the browser driver, startup lifecycle, events, plugins, and shutdown inside your own Bun application. | | `create` | The public factory that starts WhatsApp and resolves to a ready messaging `Client`. | You want to call message methods after `await create()` with browser setup and QR login handled for you. | | `OpenWAClient` | The object returned by `createClient`. | You created a core runtime and must start it, stop it, inspect readiness, access events, or connect to the transport. | Think of Easy API as the packaged service, [`create`](/docs/reference/create) as the ready messaging factory, `createClient` as the core constructor, and `OpenWAClient` as the running runtime handle. The higher-level client facade and socket clients build on top of those pieces to expose familiar message methods. The core client itself focuses on lifecycle, event flow, plugin hosting, session readiness, and browser transport access. For an embedded application, start with [`create()`](/docs/reference/create). Use Easy API for an HTTP service. Use `createClient` when you must own the runtime. Examples include a specified driver, custom plugin registration, direct readiness checks, and controlled shutdown behavior. ## Runnable example [#runnable-example] This example starts the core runtime with the Puppeteer driver. It listens for lifecycle and message events and prints the readiness snapshot. Process signals cause a controlled shutdown. ```bash bun add @open-wa/wa-automate @open-wa/driver-puppeteer ``` ```ts import { createClient } from '@open-wa/wa-automate'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; async function main() { const client = await createClient({ sessionId: 'core-example', driver: new PuppeteerDriver(), headless: true, qrTimeoutMs: 0, }); client.events.on('core.starting', () => { client.logger.info('Core runtime is starting'); }); client.events.on('client.ready', ({ sessionId }) => { client.logger.info('WhatsApp session is ready', { sessionId }); }); client.events.on('message.received', ({ message }) => { client.logger.info('Received message', { message }); }); const stop = async (reason: string) => { await client.stop(reason); process.exit(0); }; process.once('SIGINT', () => void stop('SIGINT')); process.once('SIGTERM', () => void stop('SIGTERM')); await client.start(); console.log('state:', client.getState()); console.log('readiness:', client.getReadiness()); } main().catch((error) => { console.error(error); process.exit(1); }); ``` The first run needs authentication. After the session reaches `READY`, `getReadiness()` reports `ready: true` and `exposureSafe: true`. ## Lifecycle [#lifecycle] The core lifecycle is explicit. `createClient(options)` builds the runtime object, but it does not connect to WhatsApp until you call `client.start()`. | Method or state | Meaning | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `createClient(options)` | Creates the event bus, session manager, transport, plugin host, and plugin client proxy. | | `client.start()` | Initializes the driver and opens WhatsApp Web. It completes authentication, validation, finalization, and readiness events. | | `client.stop(reason?)` | Moves the session to `STOPPED`, disposes plugins, closes the transport, and emits shutdown events. | | `client.getState()` | Returns the current core state. | | `client.getReadiness()` | Returns the readiness snapshot used to decide whether the session is safe to expose. | | `client.registerFinalizationHook(hook)` | Registers work that must run during the loaded-equivalent finalization phase before the session is marked ready. | | `client.getTransport()` | Returns the underlying transport for lower-level runtime access. | | `client.screenshot()` | Captures a page screenshot when the active driver supports it. | | `client.evaluateScript(script)` | Runs JavaScript in the WhatsApp Web page context and returns the result. | Core session states are intentionally small: | State | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------- | | `DISCONNECTED` | Initial or failed state before the runtime is usable. | | `STARTING` | Startup has begun and runtime readiness is being reset. | | `AUTHENTICATING` | WhatsApp Web is loaded and authentication is in progress. | | `READY` | Runtime validation, patch lifecycle, license lifecycle, and finalization have completed safely. | | `STOPPED` | The runtime has been shut down by `client.stop()`. | Readiness is stricter than state. Authentication can finish while runtime validation, patch handling, license handling, finalization, or transport checks remain. Examine `getReadiness().ready` first. Then, examine `pending`, `blockers`, and `requirements` to find why a session is not ready. ## CreateClientOptions [#createclientoptions] Options passed to the `createClient` function to initialize the runtime. ## OpenWAClient [#openwaclient] The primary client interface returned by `createClient`. ## Plugin type references [#plugin-type-references] Core re-exports the plugin SDK types for backward compatibility. New plugins must import these types from `@open-wa/plugin-sdk`. Core also exposes them through `@open-wa/core/plugins` for internal consumers and old integrations. ### Plugin [#plugin] The top-level plugin function type. Use `createPlugin()` from the plugin SDK to create this shape with type inference. ### PluginMeta [#pluginmeta] Plugin identity and display metadata. The `name` is also used as the plugin config key and route prefix. ### PluginInput [#plugininput] The input bundle passed to a plugin during initialization. It includes the filtered event surface, scoped logger, parsed config, session ID, and plugin client proxy. ### Hooks [#hooks] The hooks returned by a plugin. Hooks can subscribe to lifecycle events, message events, API routes, dashboard pages, tools, and cleanup. ### PluginClient [#pluginclient] The client proxy exposed to plugins. Common methods are typed, and other WhatsApp methods can be called through the dynamic `ask()` pattern. ### PluginEventEmitter [#plugineventemitter] The security-filtered event emitter exposed to plugins. Plugins can subscribe to public events, but they cannot emit events or subscribe to internal or sensitive events. ### PluginLogger [#pluginlogger] The scoped logger exposed to plugins. ### DashboardPage [#dashboardpage] Dashboard page declarations contributed by plugins. ### ToolDefinition [#tooldefinition] Tool definitions exposed by plugins for automation and AI use cases. ### ToolContext [#toolcontext] Runtime context passed to plugin tool executions. ### PluginManifest [#pluginmanifest] The manifest shape consumed by dashboard surfaces that list installed plugins. ### PluginManifestEntry [#pluginmanifestentry] One plugin entry inside the dashboard manifest. ## Plugin SDK exports [#plugin-sdk-exports] Plugin authors must import from `@open-wa/plugin-sdk`. Core uses the same contracts when it registers plugins and makes the filtered plugin runtime. | Export | Kind | Relationship to core | | --------------------- | --------- | ------------------------------------------------------------------------------------------------------- | | `createPlugin` | Factory | Creates a typed `Plugin` object that core can register through `plugins` or `pluginRefs`. | | `CreatePluginOptions` | Type | Describes the object accepted by `createPlugin`, including `meta`, optional `configSchema`, and `init`. | | `defineConfig` | Helper | Builds a Zod config schema that core uses to validate `pluginConfig` before plugin initialization. | | `z` | Re-export | Lets plugin authors define config schemas without importing Zod separately. | | `Plugin` | Type | The callable plugin contract accepted by core. | | `PluginMeta` | Type | Metadata core uses for config lookup, route names, logs, and dashboard manifests. | | `PluginInput` | Type | The scoped input bundle core passes to a plugin at registration time. | | `Hooks` | Type | The event, route, page, tool, and cleanup hooks core wires into the runtime. | | `PluginClient` | Type | The safe client proxy core gives to plugins for WhatsApp method calls. | | `PluginEventEmitter` | Type | The public-only event subscription surface core gives to plugins. | | `PluginLogger` | Type | The scoped logger core gives to each plugin. | | `DashboardPage` | Type | Dashboard declarations collected by the core plugin host. | | `ToolDefinition` | Type | Plugin tool contract used by automation and AI surfaces. | | `ToolContext` | Type | Context core passes when plugin tools execute. | | `PluginManifest` | Type | Runtime manifest shape for listing installed plugin capabilities. | | `PluginManifestEntry` | Type | One plugin record inside a manifest. | Core also exports `PluginHost`, `createEventGateway`, `getPublicEvents`, `loadPlugins`, `validatePluginConfig`, and `LoadedPlugin` from its plugin module. Those are host-side infrastructure pieces. Plugin packages usually do not need them unless they are building runtime integrations rather than ordinary plugins. # create and stop (/docs/reference/create) # create and stop [#create-and-stop] `create(options?: CreateOptions): Promise` starts WhatsApp and resolves only after authentication, runtime initialization, and messaging readiness. It selects Puppeteer by default and wraps the core runtime in the messaging `Client`. A failed startup closes the partially started client before rejecting. ```ts import { create } from '@open-wa/wa-automate'; const recipient = 'REPLACE_WITH_RECIPIENT_CHAT_ID'; const client = await create(); try { const messageId = await client.sendText(recipient, 'Hello from open-wa!'); console.log('Submitted:', messageId); } finally { await client.stop(); } ``` Use an existing chat and replace `recipient` before running the example. [`sendText`](/docs/guides/messages#send-messages) returns a validated message ID or throws `SendTextError`; an ID is not a delivery or read receipt. ## Session and login options [#session-and-login-options] | Option | Default and behavior | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `sessionId` | `session`; different names use separate default profiles. | | `userDataDir` | A private profile under `~/.open-wa/sessions`, keyed by the current project directory and session name. An explicit path takes precedence. | | `sessionDataPath` | An explicit legacy path is preserved, including the core runtime's profile derivation. Prefer `userDataDir` for new applications. | | `ephemeral` | `false`; set to `true` to discard the temporary profile on shutdown. | | `headless` | `true`; terminal login does not require a desktop. | | `qrTimeout` | `0`, which waits for QR login without a scan deadline; a number sets a timeout in seconds. | | `qrLogSkip` | `false`; set to `true` when your application handles QR presentation through a custom output sink. | | `linkCode` | Optional phone number for link-code authentication instead of scanning a QR. | | `driver` | Optional explicit driver; advanced integrations can also use [`createClient`](/docs/reference/core). | `create()` uses its supplied options and library defaults. It does not load a configuration file or `WA_*` variables implicitly. Puppeteer's browser/cache environment settings are still respected. Existing low-level `createClient` and `Client` construction remain available when you need to own startup. ## Browser provisioning [#browser-provisioning] By default, startup reuses the exact Chrome revision matched to the installed Puppeteer version. If it is missing, open-wa downloads it with progress. The cache can be shared by projects; concurrent starts coordinate installation. An interrupted download can be retried on the next start. ```ts const client = await create({ browser: { cacheDirectory: '/srv/open-wa/browser-cache', download: 'never', }, }); ``` `browser.download` is `auto` by default. Use `never` for deployments that must not download at startup. `browser.cacheDirectory` overrides Puppeteer's cache location; `browser.downloadBaseUrl` selects a browser mirror. The resolver respects Puppeteer's configured cache, skip-download settings, and proxy environment variables. To provision the same browser before starting a session: ```ts import { ensureBrowser } from '@open-wa/wa-automate'; await ensureBrowser({ browser: { cacheDirectory: '/srv/open-wa/browser-cache' }, }); ``` An explicit `executablePath` takes precedence and must point to an executable browser. Invalid paths fail with an actionable error instead of silently choosing another browser. Explicit `useChrome` retains local Chrome selection. Provisioning does not install operating-system libraries or disable the browser sandbox; a launch error identifies missing system dependencies. ## stop [#stop] `client.stop(reason?: string): Promise` drains and disposes the client's listeners, stops its runtime, and closes the browser. Put it in `finally` for a one-shot script. Stopping leaves a persistent profile available for the next run; it does not log the account out of WhatsApp. For a bot that stays connected, register shutdown handlers and call `stop()` when the process is ending. The [messages guide](/docs/guides/messages#full-bot-example) has a complete example. # ack.changed over mcp (/docs/reference/event-contracts/generated/mcp_ack_changed) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # message.received over mcp (/docs/reference/event-contracts/generated/mcp_message_received) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # session.state.changed over mcp (/docs/reference/event-contracts/generated/mcp_session_state_changed) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # ack.changed over sse (/docs/reference/event-contracts/generated/sse_ack_changed) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # group.addedToGroup over sse (/docs/reference/event-contracts/generated/sse_group_addedToGroup) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # message.any over sse (/docs/reference/event-contracts/generated/sse_message_any) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # message.received over sse (/docs/reference/event-contracts/generated/sse_message_received) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # session.logout over sse (/docs/reference/event-contracts/generated/sse_session_logout) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # session.state.changed over sse (/docs/reference/event-contracts/generated/sse_session_state_changed) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # ack.changed over webhook (/docs/reference/event-contracts/generated/webhook_ack_changed) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # group.addedToGroup over webhook (/docs/reference/event-contracts/generated/webhook_group_addedToGroup) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # message.any over webhook (/docs/reference/event-contracts/generated/webhook_message_any) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # message.received over webhook (/docs/reference/event-contracts/generated/webhook_message_received) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # session.logout over webhook (/docs/reference/event-contracts/generated/webhook_session_logout) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # session.state.changed over webhook (/docs/reference/event-contracts/generated/webhook_session_state_changed) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Event contracts (/docs/reference/event-contracts) [Download the AsyncAPI 3.1 contract](/asyncapi.json), or read the generated pages below. A running Easy API instance also serves `/meta/asyncapi.json` with its own origin and external base path. The contract covers `message.received`, `message.any`, `ack.changed`, `session.state.changed`, `group.addedToGroup` and `session.logout`. Optional runtime listeners depend on the available WhatsApp runtime. This is the initial public coverage; internal lifecycle events aren't included. Listener names such as `onMessage` identify SDK callbacks. The corresponding wire event is `message.received`, whose payload contains `{ ctx, message }`. The generated reference describes the wire payload rather than treating those names as interchangeable. ## Choose a delivery surface [#choose-a-delivery-surface] * **SSE:** open `/api/events?topics=message.received,ack.changed` with your Easy API key. Each frame has an ID, event name and JSON data containing `{ id, event, payload, ts, sessionId }`. Keepalive comments arrive every 20 seconds. The reconnect hint is three seconds; reconnecting doesn't replay earlier events. * **Webhooks:** configure the webhook integration's destination and event filter. It sends `{ webhookId, sessionId, event, payload, timestamp }`. The optional delivery journal retries pending requests after a restart and adds an `Idempotency-Key` header. Delivery can repeat or arrive out of order. * **MCP Events:** let ChatGPT Work subscribe to received messages, acknowledgements or session state changes. OpenWA sends compact summaries to a verified HTTPS callback using Standard Webhooks signatures. [Configure MCP Events](/docs/client-and-integrations/mcp-events). SSE uses the supplied event bridge. The webhook plugin filters internal and sensitive events through its event gateway. MCP only exposes the three explicitly registered subscription events. The transports don't share an identical event inventory or policy. Timestamps inside SSE and ordinary webhook envelopes are Unix milliseconds. MCP event timestamps are ISO 8601 strings describing when the runtime event occurred. MCP callbacks use a separate signing timestamp. # Event Reference (/docs/reference/events) [Browse the generated wire-event contracts](/docs/reference/event-contracts) or [download AsyncAPI 3.1](/asyncapi.json) for SSE, webhooks and MCP Events. # Event Reference [#event-reference] Each open-wa client has its own `HyperEmitter` event stream. The shared `ev` helper forwards native events from those clients and supports v4 wildcard subscriptions through the Effect-backed HyperEmitter hybrid. Effect owns each forwarding connection and removes it when the session closes. The Effect hybrid and optional PubSub APIs are available from OpenWA 5.6.0. See [session events](/docs/guides/session-events) for aliases and payloads, the [HyperEmitter reference](/docs/reference/hyperemitter) for emitter methods, and [Consume events with Effect](/docs/guides/effect-event-streams) for a complete callback-and-queue example. ## Listener examples [#listener-examples] Import the shared emitter when you need process-level lifecycle events: ```ts import { ev } from '@open-wa/wa-automate'; ``` Listen for common runtime and WhatsApp activity events by name: ```ts ev.on('client.ready', ({ sessionId }) => { console.log(`Session ${sessionId} is ready`); }); ev.on('message.received', async ({ message, ctx }) => { console.log('Message received', ctx.correlationId, message); }); ev.on('session.connection.disconnected', ({ details }) => { console.warn('Session disconnected', details?.reason); }); ev.on('error', ({ scope, error, fatal }) => { console.error(`Open-WA error in ${scope}`, { fatal, error }); }); ``` Use client-level helpers, such as `client.onMessage`, for normal bot behavior after the client is ready. Use `ev` when you need startup, auth, disconnect, webhook, persistence, or internal runtime events. ## Wildcard example [#wildcard-example] The shared emitter supports wildcard listeners. This is useful for debugging or for carefully redacted audit sinks. Some compatibility and session-level emitters also pass session data through wildcard segments: ```ts ev.on('**', (payload, sessionId, namespace) => { auditEvent({ namespace, sessionId, receivedAt: Date.now(), }); }); ``` Keep wildcard listeners narrow in production. QR codes, link codes, session artifacts, and persistence events can contain sensitive data. ## OpenWAEventMap [#openwaeventmap] This interface defines all available events and their corresponding payload shapes. ## Full event names [#full-event-names] The table below lists every event name in `OpenWAEventMap` with its namespace. Event names without a dot are process-level compatibility or utility events. | Namespace | Events | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `launch.create` | `launch.create.start`, `launch.create.retry`, `launch.create.ready` | | `launch.config.setup` | `launch.config.setup.before`, `launch.config.setup.after` | | `launch.logging.setup` | `launch.logging.setup.before`, `launch.logging.setup.after` | | `launch.update.check` | `launch.update.check.before`, `launch.update.check.after` | | `launch.browser.init` | `launch.browser.init.before`, `launch.browser.init.after` | | `launch.page.init` | `launch.page.init.before`, `launch.page.init.after` | | `launch.page.interception` | `launch.page.interception.before`, `launch.page.interception.after` | | `launch.navigation.gotoWaWeb` | `launch.navigation.gotoWaWeb.before`, `launch.navigation.gotoWaWeb.after` | | `launch.modules.wait` | `launch.modules.wait.before`, `launch.modules.wait.after` | | `launch.injection.earlyCheck` | `launch.injection.earlyCheck.before`, `launch.injection.earlyCheck.after` | | `launch.auth` | `launch.auth.check.before`, `launch.auth.check.after`, `launch.auth.nuke.detected`, `launch.auth.timeout`, `launch.auth.phoneOutOfReach`, `launch.auth.qr.requested`, `launch.auth.qr.generated`, `launch.auth.qr.scanned`, `launch.auth.qr.expired`, `launch.auth.linkCode.requested`, `launch.auth.linkCode.generated`, `launch.auth.pairing`, `launch.auth.syncing` | | `launch.license.preload` | `launch.license.preload.before`, `launch.license.preload.after` | | `launch.wapi.inject` | `launch.wapi.inject.before`, `launch.wapi.inject.after` | | `launch.helper.pre_api` | `launch.helper.pre_api.before`, `launch.helper.pre_api.after` | | `launch.session` | `launch.session.validityCheck.before`, `launch.session.validityCheck.after`, `launch.session.invalid.retry` | | `launch.license.check` | `launch.license.check.before`, `launch.license.check.after` | | `launch.patch.init` | `launch.patch.init.before`, `launch.patch.init.after` | | `launch.patch.integrity` | `launch.patch.integrity.before`, `launch.patch.integrity.after` | | `launch.client.finalize` | `launch.client.finalize.before`, `launch.client.finalize.after` | | `session.state` | `session.state.changed` | | `session.connection` | `session.connection.disconnected`, `session.connection.reconnecting`, `session.connection.reconnected` | | `session.frame` | `session.frame.navigated` | | `session.reinject` | `session.reinject.detected`, `session.reinject.before`, `session.reinject.after`, `session.reinject.qr.waiting` | | `session.stale` | `session.stale.detected` | | `session` | `session.logout`, `session.data` | | `message` | `message.received`, `message.any`, `message.deleted` | | `event` | `event.changed`, `event.response` | | `ack` | `ack.changed` | | `group` | `group.addedToGroup`, `group.removedFromGroup`, `group.participants.changed.global`, `group.approval.request`, `group.changed` | | `chat` | `chat.deleted`, `chat.opened`, `chat.state` | | `device` | `device.battery`, `device.plugged` | | `call` | `call.incoming`, `call.state` | | `auth` | `auth.logout` | | `ui` | `ui.button`, `ui.poll.vote` | | `broadcast` | `broadcast.received` | | `label` | `label.changed` | | `story` | `story.received` | | `commerce` | `commerce.order`, `commerce.product.new` | | `reaction` | `reaction.added` | | `media.preprocess` | `media.preprocess.before`, `media.preprocess.after` | | `webhook` | `webhook.registered`, `webhook.unregistered`, `webhook.trigger.before`, `webhook.trigger.after`, `webhook.queue.enqueued`, `webhook.queue.dequeued`, `webhook.deliver.before`, `webhook.deliver.after` | | `persistence.session.save` | `persistence.session.save.before`, `persistence.session.save.after` | | `persistence.compress.zstd` | `persistence.compress.zstd.before`, `persistence.compress.zstd.after` | | `persistence.archive.zip` | `persistence.archive.zip.before`, `persistence.archive.zip.after` | | `persistence.upload.s3` | `persistence.upload.s3.before`, `persistence.upload.s3.after` | | `patch.apply` | `patch.apply.before`, `patch.apply.after` | | `patch.init` | `patch.init.before`, `patch.init.after` | | `license.check` | `license.check.before`, `license.check.after` | | `license.inject` | `license.inject.before`, `license.inject.after` | | `cli` | `cli.launched` | | `server.start` | `server.start.before`, `server.start.after` | | `server.port` | `server.port.live` | | `core` | `core.starting`, `core.started`, `core.stopping`, `core.stopped` | | `client` | `client.ready` | | Process-level | `internal_launch_progress`, `critical_internal_message`, `error` | ## Payload samples [#payload-samples] The generated table above is the source of truth for payload shapes. These samples show the structure you will usually see in logs or listeners. ### `client.ready` [#clientready] ```json { "sessionId": "sales" } ``` ### `message.received` [#messagereceived] ```json { "ctx": { "correlationId": "evt_01HX9Q8K7V3P", "ts": 1715683200000 }, "message": { "id": "true_447700900123@c.us_3EB0F", "from": "447700900123@c.us", "body": "Hi" } } ``` ### `launch.auth.qr.generated` [#launchauthqrgenerated] ```json { "correlationId": "launch_01HX9Q8K7V3P", "attempt": 1, "ts": 1715683200000, "step": "launch.auth.qr.generated", "details": { "qr": "2@abc...", "ascii": "████ QR ████", "attemptInThisCycle": 1 } } ``` ### `session.connection.disconnected` [#sessionconnectiondisconnected] ```json { "correlationId": "session_01HX9Q8K7V3P", "ts": 1715683300000, "step": "session.connection.disconnected", "details": { "reason": "browser_closed", "wasLoggedIn": true } } ``` ### `webhook.trigger.after` [#webhooktriggerafter] ```json { "ctx": { "correlationId": "webhook_01HX9Q8K7V3P", "ts": 1715683400000 }, "webhookId": "primary-crm", "event": "message.received", "enqueued": true } ``` ## Lifecycle ordering [#lifecycle-ordering] A normal first login usually follows this order: 1. `core.starting` 2. `launch.create.start` 3. `launch.config.setup.before` 4. `launch.config.setup.after` 5. `launch.browser.init.before` 6. `launch.browser.init.after` 7. `launch.page.init.before` 8. `launch.page.init.after` 9. `launch.navigation.gotoWaWeb.before` 10. `launch.navigation.gotoWaWeb.after` 11. `launch.auth.check.before` 12. `launch.auth.qr.generated`, when a new login needs a QR 13. `launch.auth.qr.scanned`, after the QR is scanned 14. `launch.auth.check.after`, when auth state is known 15. `launch.wapi.inject.before` 16. `launch.wapi.inject.after` 17. `launch.session.validityCheck.before` 18. `launch.session.validityCheck.after` 19. `launch.client.finalize.before` 20. `launch.client.finalize.after` 21. `session.state.changed`, usually to `READY` 22. `client.ready` 23. `core.started` Warm starts can skip QR events. Failed starts can emit retry, timeout, invalid session, or error events before the lifecycle reaches `client.ready`. ## Mapping to session-events guide [#mapping-to-session-events-guide] This reference lists the typed event names and payloads from the core event map. For a task-oriented guide to QR capture, session data handling, lifecycle timing, and wildcard patterns, read **[Session events](/docs/guides/session-events)**. ## Common Payloads [#common-payloads] ### StepEvent [#stepevent] Most internal launch and lifecycle events use the `StepEvent` wrapper. ### EventContext [#eventcontext] Standard metadata included with almost every event. # HyperEmitter and Effect (/docs/reference/hyperemitter) # HyperEmitter and Effect [#hyperemitter-and-effect] The Effect hybrid and optional PubSub APIs on this page are available from OpenWA 5.6.0. `@open-wa/hyperemitter` exports two emitters. OpenWA's shared `ev` uses the `EventEmitter` facade; `client.events` uses the typed `HyperEmitter`. | Interface | `EventEmitter` / `ev` | `HyperEmitter` / `client.events` | | ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------- | | Callback arguments | `(...args)`; native global events pass `(payload, sessionId, namespace)` | One typed payload | | Default wildcards | `*` matches one segment; `**` matches zero or more segments, anywhere | `+` matches one segment; trailing `#` matches zero or more segments | | Queued frame | `{ event, args }` | `{ event, payload }`, with the event and payload types linked | | Await listener work | `emitAsync()` or `emitEffect()` | `emitEffect()` | For a runnable example, read [Consume events with Effect](/docs/guides/effect-event-streams). For WhatsApp payloads and session aliases, read [Event Reference](/docs/reference/events) and [Session events](/docs/guides/session-events). ## Dispatch and listener ownership [#dispatch-and-listener-ownership] | Method | Behavior | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `on(event, listener, options?)` | Registers a callback immediately. Ordinary callbacks run synchronously during `emit()`. | | `once(event, listener, options?)` | Removes the registration before its first invocation, including recursive emissions. | | `off(event, listener)` | Removes a registration using the original callback reference. | | `onScoped(event, listener, options?)` | Returns an Effect requiring `Scope`. Running it registers the listener; closing the scope removes it and interrupts its in-flight work. | | `onAnyScoped(listener, options?)` | Owns a catch-all listener in the same way. Its callback receives `(event, ...args)` on `ev`, or `(event, payload)` on `HyperEmitter`. | | `emit(event, ...args)` / `emit(event, payload)` | Publishes one observation if the hub exists, then invokes matching callbacks. Returns whether callback listeners were found, even if the queued hub accepted the event. | | `emitEffect(event, ...args)` / `emitEffect(event, payload)` | Returns a lazy Effect that dispatches and awaits direct listener work. No event is emitted until the Effect runs. | | `close()` | Returns an Effect that closes the emitter, removes listeners, interrupts its owned work, and shuts down its PubSub. It doesn't promise to drain queued events. | | `isClosed` | Reports whether the owning emitter has closed. A closed emitter rejects new registrations. | Listeners may return a Promise or an Effect. `emit()` starts that work in fibers owned by the emitter or scoped registration; it returns before asynchronous completion. `emitEffect()` waits and exposes uncaptured listener failures through its error channel. Cancelling an Effect cannot undo an external side effect already started by a Promise; the operation must support cancellation itself. The public facade also keeps the established callback methods: `addListener`, `prependListener`, `prependOnceListener`, `many`, `prependMany`, `onAny`, `prependAny`, `offAny`, `removeListener`, `removeAllListeners`, `listeners`, `listenersAny`, `listenerCount`, `hasListeners`, `eventNames`, `setMaxListeners`, `getMaxListeners`, `waitFor`, `listenTo`, and `stopListeningTo`. Event names can be strings, symbols, or namespace arrays. Ordinary functions receive the emitter as `this`, with the current name in `this.event`. `await ev.emitAsync(event, ...args)` returns direct callback results in listener order. Neither `emitAsync()` nor `emitEffect()` waits for PubSub consumers. Those consumers must define their own acknowledgement, timeout, and retry policy. `onGlobalEvent(event, listener)` from `@open-wa/wa-automate` is the shared `ev.onScoped()` convenience helper. Stopping a client closes its local emitter and detaches its global forwarding connection. It leaves the process-level `ev` and other sessions' subscriptions open. Close `ev` only when your application owns the lifetime of the entire process-level feed. ## PubSub methods [#pubsub-methods] Both emitters expose these methods: | Method | Return value and behavior | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `pubsub(options?)` | A native `PubSub.PubSub`. Creates the hub synchronously on first access; later calls return the same hub. | | `subscribe(options?)` | `Effect, never, Scope>`. Creates the hub and subscription when run; the subscription is released when its scope closes. | | `stream(options?)` | `Stream`. Subscribes when consumed, and releases its subscription when consumption ends. | | `hasPubSub` | True while the hub exists and is active. Reading it doesn't create a hub. | | `pubsubStatus` | `{ enabled, shutdown, strategy, accepted, rejected }`. Reading it doesn't create a hub. | Queued envelopes and argument/namespace arrays are frozen. Payload objects keep their original identity and aren't deep-frozen or cloned. An observation is published before callback invocation, so recursive emissions keep their original queued order. The frame records an emission, including one whose callbacks fail. `PubSub.publish(ev.pubsub(), frame)` publishes directly to queued readers. It doesn't invoke callbacks or update the emitter's counters. Use `emit()`, `emitAsync()`, or `emitEffect()` to deliver through both interfaces. The owner shuts down the hub on `close()`, which interrupts waiting readers. Calling native `PubSub.shutdown(hub)` yourself also ends queued observation; ordinary callback dispatch continues while the emitter remains open. Keep hub shutdown with its owner when sharing it between modules. ## PubSub options [#pubsub-options] Choose options before the first `pubsub()`, running `subscribe()`, or consuming `stream()`. Conflicting options after configuration throw an error; the hub is never silently replaced. | Option | Default | Meaning | | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `strategy` | `'unbounded'` | `'unbounded'`, `'dropping'`, or `'sliding'`. All keep synchronous producers from waiting for queued readers. | | `capacity` | None | Required positive integer for dropping or sliding. It is invalid for unbounded. | | `replay` | `0` | Non-negative integer number of recent frames retained for later subscribers, independently of the active-subscriber capacity policy. | Unbounded queues retain pending frames for active subscribers without a memory cap. Dropping rejects a new observation when full. Sliding evicts older pending observations to accept newer ones. Replay adds retention even without an active subscriber, so size it deliberately. `pubsubStatus.accepted` and `.rejected` count publications made through emitter methods. Acceptance isn't delivery completion. Sliding evictions don't increment `rejected`, and raw native publications aren't counted. After shutdown, `enabled` still records that a hub was created; use `shutdown` or `hasPubSub` for its active state. Finite lossless backpressure requires an effectful producer that can suspend. Use an integration-owned PubSub or Queue for that workflow; it isn't an option beneath synchronous `emit()`. # API Reference (/docs/reference) # API Reference [#api-reference] This section is sourced from the active generated reference set. It is intentionally kept separate from the hand-written guides so implementation details stay close to the code surface they document. ## Use this section when you need [#use-this-section-when-you-need] * exact method signatures * event and enum names * config/interface shapes * generated class, type, and module pages For onboarding and task-oriented examples, start with the hand-written guides instead. ## Navigation explanation [#navigation-explanation] The reference docs are organized by the surface you are trying to inspect. * **Core** covers the primary `createClient` options and the `OpenWAClient` interface returned by the embedded runtime. * **Messaging** covers shared message, chat, contact, and content shapes used across methods and events. * **Events** covers the event map and common event payloads emitted by the runtime. * **Client API Reference** contains generated Easy API client method pages. It groups them into namespaces for messages, chats, groups, contacts, media, sessions, status, labels, business, and communities. Use the top-level reference pages when you need shared types. Use the client sub-sections when you need a specific Easy API method, its route, parameters, aliases, and return shape. ## HTTP route conventions [#http-route-conventions] Client method pages list the primary HTTP route for each method and any supported alias routes. Current Easy API command routes are mounted under `/api/`. Method routes follow the schema registry metadata. Namespaced routes use the namespace first, then a short action path, for example `POST /api/messages/sendText` or `GET /api/messages/get`. Alias routes keep compatibility with older or direct method names, for example `POST /api/sendText`. HTTP methods map to the action being performed: * `GET` is used for read and lookup methods. * `POST` is used for send, create, update, and command methods with a body. * `DELETE` is used for delete methods. Route segments can use generated names, compatibility aliases, or shorter action names. Treat the listed `HTTP route` as the preferred route for the current build, and treat `HTTP alias routes` as compatibility paths when they are shown. ## Auth requirements [#auth-requirements] Easy API endpoints need the configured API key when the runtime starts with `--api-key`. Send that key with each request in the `X-API-Key` header. ```bash curl -X POST http://localhost:8080/api/sendText \ -H "Content-Type: application/json" \ -H "X-API-Key: your-secure-key" \ -d '{"to":"1234567890@c.us","text":"Hello from open-wa!"}' ``` Some local discovery or health pages can accept a different request shape. Treat command routes as authenticated API calls. If a request returns `401`, make sure that the key is present and matches the Easy API process. A `403` means that authentication succeeded, but the endpoint refused the request. ## Request body conventions [#request-body-conventions] Request bodies use JSON objects whose keys match the parameter names listed in each generated method table. For `POST` and other body-based routes, include `Content-Type: application/json` and pass the necessary fields shown in the table. ```json { "to": "1234567890@c.us", "text": "Hello from open-wa!" } ``` Parameter tables also list key aliases and deprecated key aliases when they exist. Prefer the primary parameter name unless you are maintaining older code that already depends on an alias. The `Positional parameter order` line is mainly useful when comparing HTTP calls with client method calls such as `sendText(to, text)`. For `GET` routes with parameters, pass the listed values as query parameters unless the page or generated schema for that method says otherwise. ## Generated-reference limitations [#generated-reference-limitations] Most client method pages are generated from the active schema registry. That keeps the reference close to the code, but it also means the generated text can have gaps, awkward wording, repeated aliases, or formatting issues. Use generated pages for exact names, routes, parameter keys, and return shapes. Use the hand-written guides for workflow examples. If a guide and reference disagree, use the reference for the current method surface. Verify it against your runtime version. ## Examples [#examples] Here are a few practical ways to use this section. ### Find the route for sending a message [#find-the-route-for-sending-a-message] Open **Client API Reference** and then **Messages**. Look for `sendText`, confirm the listed HTTP route, check necessary parameters, and copy the JSON keys from the parameter table. ### Check which object shape an event returns [#check-which-object-shape-an-event-returns] Open **Events** to find the event name and payload type. If the payload includes a message, open **Messaging** to inspect the shared `Message` shape. ### Compare embedded runtime and Easy API usage [#compare-embedded-runtime-and-easy-api-usage] Use **Core** when you are calling `createClient` directly in your Node.js process. Use **Client API Reference** when you are calling a running Easy API instance over HTTP or through `SocketClient`. ### Confirm aliases before changing old code [#confirm-aliases-before-changing-old-code] Open the generated client method page and check `HTTP alias routes`, `Aliases`, and `Deprecated aliases`. Move new code toward the preferred route and primary parameter names, but keep aliases in mind when maintaining older integrations. # Messaging Reference (/docs/reference/messaging) # Messaging Reference [#messaging-reference] The `Message` object is the most common data structure in Open-WA. It contains all information about a WhatsApp message, including content, sender, and media metadata. ## Message [#message] The core message structure used in events and method returns. ## Chat [#chat] Represents a WhatsApp chat session (individual or group). ## Contact [#contact] Represents a WhatsApp contact, either a single user or a participant in a group. ## Real examples of Message/Chat/Contact [#real-examples-of-messagechatcontact] These examples show common runtime shapes. WhatsApp can add extra fields, so store the fields you need and treat the payload as extensible. ### Message example [#message-example] A text message includes routing fields, sender details, chat details, direction, timestamps, and message state. ```json { "id": "false_447700900000@c.us_3EB0A1B2C3D4E5F67890", "body": "Hi, is my order ready?", "type": "chat", "t": 1768848000, "timestamp": 1768848000, "from": "447700900000@c.us", "to": "15551234567@c.us", "chatId": "447700900000@c.us", "fromMe": false, "self": "in", "ack": 1, "isGroupMsg": false, "isMedia": false, "isNotification": false, "isQuotedMsgAvailable": false, "sender": { "id": "447700900000@c.us", "pushname": "Aisha", "isMyContact": true, "isWAContact": true }, "chat": { "id": "447700900000@c.us", "name": "Aisha", "isGroup": false, "unreadCount": 1, "contact": { "id": "447700900000@c.us", "pushname": "Aisha", "isWAContact": true } } } ``` ### Chat example [#chat-example] A one-to-one chat usually points at the contact chat ID. A group chat uses an `@g.us` ID and can include `groupMetadata`. ```json { "id": "447700900000@c.us", "name": "Aisha", "formattedTitle": "Aisha", "isGroup": false, "unreadCount": 1, "t": 1768848000, "contact": { "id": "447700900000@c.us", "pushname": "Aisha", "isMyContact": true, "isWAContact": true }, "msgs": [] } ``` ### Contact example [#contact-example] Contact display fields are optional. Use `id` as the stable identity for lookups and message targets. ```json { "id": "447700900000@c.us", "name": "Aisha Khan", "shortName": "Aisha", "pushname": "Aisha", "formattedName": "Aisha Khan", "isBusiness": false, "isEnterprise": false, "isMe": false, "isMyContact": true, "isUser": true, "isWAContact": true, "labels": [], "profilePicThumbObj": { "eurl": "https://mmg.whatsapp.net/...", "img": "https://mmg.whatsapp.net/...", "tag": "profile-thumb" } } ``` ## Message Content Types [#message-content-types] ### Media message [#media-message] Media messages are still `Message` objects. The `type` field identifies the media kind, and `isMedia` is the safest first check before downloading or decrypting content. ```json { "id": "false_447700900000@c.us_3EB09988776655443322", "body": null, "caption": "Receipt from today", "type": "image", "mimetype": "image/jpeg", "t": 1768848240, "timestamp": 1768848240, "from": "447700900000@c.us", "to": "15551234567@c.us", "chatId": "447700900000@c.us", "fromMe": false, "self": "in", "ack": 1, "isGroupMsg": false, "isMedia": true, "isMMS": true, "isNotification": false, "isQuotedMsgAvailable": false, "clientUrl": "https://mmg.whatsapp.net/...", "deprecatedMms3Url": "https://mmg.whatsapp.net/...", "mediaData": { "type": "image", "mediaStage": "RESOLVED", "mimetype": "image/jpeg", "filehash": "8f2b5f7b4c0a1d2e3f4a5b6c7d8e9f00", "size": 248391 } } ``` Common media fields include: | Field | Meaning | | --------------------------------- | --------------------------------------------------------------------------------- | | `type` | Message type, such as `image`, `video`, `audio`, `ptt`, `document`, or `sticker`. | | `isMedia` / `isMMS` | Flags that mark messages with downloadable media metadata. | | `mimetype` | MIME type for the encrypted media, such as `image/jpeg` or `application/pdf`. | | `caption` | Optional text attached to image, video, or document messages. | | `mediaData` | Runtime metadata used to locate and decrypt encrypted media. | | `clientUrl` / `deprecatedMms3Url` | Media URLs that can appear on some payloads. Treat them as runtime details. | Do not read file bytes from `body`. Use `caption` for attached text and pass the message to media helpers when you need the content. ### Location [#location] Geographical data associated with location messages. ### Location message [#location-message] A location message is a `Message` object with `type: "location"`. Depending on the runtime version, coordinates can appear directly on the message or inside a location-shaped field. ```json { "id": "false_447700900000@c.us_3EB01234ABCD5678EF90", "body": "Shared location", "type": "location", "t": 1768848360, "timestamp": 1768848360, "from": "447700900000@c.us", "to": "15551234567@c.us", "chatId": "447700900000@c.us", "fromMe": false, "self": "in", "ack": 1, "isGroupMsg": false, "isMedia": false, "isNotification": false, "isQuotedMsgAvailable": false, "lat": 51.5074, "lng": -0.1278, "loc": "London", "clientUrl": "https://maps.google.com/?q=51.5074,-0.1278", "sender": { "id": "447700900000@c.us", "pushname": "Aisha" } } ``` The normalized `Location` shape uses these fields: ```json { "lat": 51.5074, "lng": -0.1278, "name": "London", "address": "London, UK", "url": "https://maps.google.com/?q=51.5074,-0.1278" } ``` ## Field caveats [#field-caveats] The generated tables describe the common contract, but live WhatsApp payloads vary by message type, account state, and runtime version. | Field | Caveat | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `body` | Usually text for `chat` messages. It can be empty or null for media, location, notification, revoked, or system messages. | | `caption` | Only appears when the sender included caption text on supported media types. | | `sender`, `chat`, `quotedMsg`, `quotedMsgObj` | Nested objects can be partial snapshots. Quote fields only appear when WhatsApp still exposes the quoted message. | | `author` / `senderId` | Most useful in groups. They can be absent in one-to-one messages or older payloads. | | `notifyName`, `name`, `pushname`, `formattedName` | Display names depend on what the session knows about the contact. Do not use them as stable IDs. | | `mimetype`, `mediaData`, `clientUrl`, `deprecatedMms3Url` | Media-only runtime fields. Some versions expose one URL field, the two URL fields, or neither. | | `lat`, `lng`, `loc`, `name`, `address`, `url` | Location fields differ across raw message payloads and normalized location objects. Check direct message fields and location-shaped data when handling older payloads. | | Extra fields | The schemas use passthrough objects, so additional WhatsApp or driver fields can be present. Preserve unknown fields if your workflow needs full fidelity. | # @open-wa/cli-app (/docs/reference/workspaces/apps/cli) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/cli/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/cli/README.md) # @open-wa/cli-app [#open-wacli-app] WhatsApp CLI - Easy API mode for @open-wa/wa-automate Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/cli-app build` * `pnpm --filter @open-wa/cli-app dev` * `pnpm --filter @open-wa/cli-app lint` * `pnpm --filter @open-wa/cli-app start` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/dashboard-neo (/docs/reference/workspaces/apps/dashboard-neo) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/dashboard-neo/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/dashboard-neo/README.md) # TanStack Start + shadcn/ui [#tanstack-start--shadcnui] This is a template for a new TanStack Start project with React, TypeScript, and shadcn/ui. ## Adding components [#adding-components] To add components to your app, run the following command: ```bash npx shadcn@latest add button ``` This will place the ui components in the `components` directory. ## Use components [#use-components] To use the components in your app, import them as follows: ```tsx import { Button } from "@/components/ui/button"; ``` # @open-wa/docker (/docs/reference/workspaces/apps/docker) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/docker/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/docker/README.md) # wa-automate-docker [#wa-automate-docker] This app runs the Open-WA API server in a Docker container. The Compose example binds the API to `127.0.0.1`, pins the Open-WA version, and stores the browser profile in a named volume. ## Instructions [#instructions] Set an API key and start the Compose service from this directory: ```bash export WA_API_KEY='replace-with-a-long-random-key' docker compose up -d ``` Open-WA listens on port `8080` inside the container, and Compose publishes it on `127.0.0.1:8080`. The `sessions` volume preserves the browser profile when the container restarts. Keep the API on loopback or a private network unless you have configured an appropriate access boundary. ## Versioning [#versioning] The Compose file sets `W_A_V` to `5.1.0`; update that value when you choose another published [wa-automate version](https://github.com/open-wa/wa-automate-nodejs/releases). The container entrypoint installs that version when it starts. # docs (/docs/reference/workspaces/apps/docs) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/docs/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/docs/README.md) # docs [#docs] Fumadocs and TanStack Start documentation site for the open-wa v5 monorepo. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter docs dev` * `pnpm --filter docs build` * `pnpm --filter docs start` * `pnpm --filter docs types` * `pnpm --filter docs check` * `pnpm --filter docs lint` * `pnpm --filter docs workspace-docs` * `pnpm --filter docs format` * `pnpm --filter docs copy` * `pnpm --filter docs openapi` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## Blog and changelog code examples [#blog-and-changelog-code-examples] EmDash content uses Markdown, not executable MDX. The shared editorial renderer uses Fumadocs `DynamicCodeBlock` for highlighting and copy buttons, and `CodeBlockTabs` for adjacent fences with `tab` metadata. Use `title` for the filename, runtime, or command label: ````md ```ts tab="SDK · TypeScript" tab-group="openwa-api-examples" title="Node.js SDK" const id = await client.sendText('447700900123@c.us', 'Hello'); ``` ```bash tab="Easy API · cURL" title="HTTP request" curl --fail-with-body "$OPENWA_API_URL/api/messages/sendText" \ -H "X-API-Key: $OPENWA_API_KEY" \ -H 'Content-Type: application/json' \ --data '{"to":"447700900123@c.us","content":"Hello"}' ``` ```js tab="Easy API · JavaScript" title="Node.js fetch" // Complete JavaScript HTTP example. ``` ```python tab="Easy API · Python" title="Python · requests" # Complete Python HTTP example. ``` ```` Keep the fences adjacent, with only blank lines between them. Use the same four labels for API examples: `tab-group` remembers the reader's choice across those examples. For a different set of tabs, choose a different group ID or omit it. Single fences also support `title` and have copy controls. Each feature article must explain Easy API startup, pairing, API-key headers, and the canonical route and named JSON fields from the shipped schema. HTTP method results use `{ success, data }`; host controls such as session refresh have their own response shape. Document event-stream topics and envelope fields for listeners. State which machine owns file paths, and provide working HTTP examples rather than serializing SDK methods or streams. Use standalone terminal examples for operations that have no HTTP equivalent. ## Publishing [#publishing] `.github/workflows/docs-run.yml` builds the schema and documentation, applies feedback migrations, and deploys `openwa-docs` to `https://openwa.dev`. Changes to the docs, their schema/client/UI sources, workspace READMEs and package manifests on `master` trigger publication. Dependency changes also rebuild the site. The Bumpy release workflow explicitly dispatches the same deployment from `release`, so versioned API contracts publish after the package release too. The job uses the GitHub `Docs` environment's `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` secrets. The deployment token is account-owned, with Individual Workers Editor access to `openwa-docs` and D1 Write permission for feedback migrations. It does not require access to other Workers or DNS. To republish the current source without publishing npm packages: ```sh gh workflow run docs-run.yml --repo open-wa/wa-automate-nodejs --ref master ``` Successful package publication and successful documentation deployment are separate outcomes. Inspect the docs workflow's Cloudflare deployment step when the live site is stale. ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # Apps (/docs/reference/workspaces/apps) # Apps [#apps] The docs generator creates these pages from direct child README files under `apps/`. * [@open-wa/cli-app](./cli) - WhatsApp CLI - Easy API mode for @open-wa/wa-automate * [@open-wa/dashboard-neo](./dashboard-neo) - Static dashboard UI for the open-wa v5 Easy API. * [@open-wa/docker](./docker) - This is a simple app that will expose an API that you can use to control your whatsapp instance. * [docs](./docs) - docs workspace in the open-wa v5 monorepo. * [orchestrator-cli](./orchestrator-cli) - orchestrator-cli workspace in the open-wa v5 monorepo. * [orchestrator-dashboard](./orchestrator-dashboard) - orchestrator-dashboard workspace in the open-wa v5 monorepo. * [registry](./registry) - registry workspace in the open-wa v5 monorepo. # orchestrator-cli (/docs/reference/workspaces/apps/orchestrator-cli) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/orchestrator-cli/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/orchestrator-cli/README.md) # @open-wa/orchestrator-cli [#open-waorchestrator-cli] Multi-session orchestration CLI Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/orchestrator-cli build` * `pnpm --filter @open-wa/orchestrator-cli dev` * `pnpm --filter @open-wa/orchestrator-cli lint` * `pnpm --filter @open-wa/orchestrator-cli start` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # orchestrator-dashboard (/docs/reference/workspaces/apps/orchestrator-dashboard) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/orchestrator-dashboard/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/orchestrator-dashboard/README.md) # @open-wa/orchestrator-dashboard [#open-waorchestrator-dashboard] TanStack Router dashboard for managing open-wa orchestrator sessions. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/orchestrator-dashboard dev` * `pnpm --filter @open-wa/orchestrator-dashboard build` * `pnpm --filter @open-wa/orchestrator-dashboard start` * `pnpm --filter @open-wa/orchestrator-dashboard check` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # registry (/docs/reference/workspaces/apps/registry) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`apps/registry/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/apps/registry/README.md) # registry [#registry] Static component registry for open-wa UI assets, served from `public/registry.json`. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter registry dev` * `pnpm --filter registry build` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # Workspace Reference (/docs/reference/workspaces) # Workspace Reference [#workspace-reference] The docs generator creates this section from direct child README files under `apps/`, `packages/`, and `integrations/`. It includes 7 app workspaces, 32 package workspaces, and 7 integration workspaces. * [Apps](./apps) * [Packages](./packages) * [Integrations](./integrations) Run `pnpm --filter docs workspace-docs` to refresh these pages after changing a workspace README. # @open-wa/integration-chatwoot (/docs/reference/workspaces/integrations/chatwoot) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/chatwoot/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/chatwoot/README.md) # @open-wa/integration-chatwoot [#open-waintegration-chatwoot] `@open-wa/integration-chatwoot` bridges WhatsApp messages with a Chatwoot API inbox. The current plugin package is `5.1.0`. It initializes the Chatwoot client at `core.started`, syncs supported WhatsApp messages into Chatwoot, and exposes `createChatwootRouter` for Chatwoot webhooks. ## Quick start [#quick-start] Install the plugin in the open-wa application that owns the WhatsApp session, then configure it under `pluginConfig.chatwoot`: ```ts export default { port: 8080, host: '0.0.0.0', apiKey: 'your-secure-key', plugins: ['@open-wa/integration-chatwoot'], pluginConfig: { chatwoot: { chatwootUrl: 'https://app.chatwoot.com/api/v1/accounts/123', chatwootApiAccessToken: 'your-chatwoot-user-access-token', apiHost: 'https://wa.example.com', apiKey: 'your-secure-key', forceUpdateCwWebhook: true, }, }, }; ``` `chatwootUrl` may include an existing inbox, for example `.../accounts/123/inboxes/456`. Otherwise the client finds or creates an API inbox. `apiHost` is the public origin Chatwoot can reach; the plugin adds `/plugins/chatwoot/webhook` and, when `apiKey` is present, appends it as a query value. The callback router currently parses the webhook body but does not validate that `api_key` query value itself. Put a reverse proxy or gateway in front of the callback when it must be authenticated, and have that boundary validate the query or an equivalent header before forwarding to open-wa. ## Configuration [#configuration] | Field | Required | Behavior | | ------------------------ | -------- | ------------------------------------------------------------------------------------ | | `chatwootUrl` | Yes | Chatwoot origin plus account ID and optional inbox ID. | | `chatwootApiAccessToken` | Yes | Sent as `api_access_token` on Chatwoot API requests. | | `apiHost` | No | Public origin used to build the Chatwoot webhook URL. | | `host`, `https`, `port` | No | Alternative callback host settings used when `apiHost` is absent. | | `apiKey` | No | Appended to the callback URL as `api_key`; enforce it at a proxy or gateway. | | `forceUpdateCwWebhook` | No | Defaults to `false`; when true, patches the inbox webhook URL during initialization. | ## Message behavior and limits [#message-behavior-and-limits] * Direct WhatsApp messages create or reopen contacts and conversations. Group, broadcast, and echo messages are ignored. * Text replies from Chatwoot use `sendText`. Replies containing a URL use link preview support. * A reply beginning with a token such as `@51.5072,-0.1276 Meeting point` becomes a WhatsApp location. * Chatwoot attachments are sent with `sendImage`, using the Chatwoot message content as the first caption. * Inbound image, audio, voice note, video, and document messages become Chatwoot attachments when open-wa can decrypt the media and the required media fields are present. When only `cloudUrl` is available, Chatwoot receives a file link in text. * Contact and conversation registries are in memory. Restarting the process rebuilds them from later messages and the configured inbox state. The integration marks outbound message IDs as ignored so a reply does not loop back as a new inbound message. It does not provide a durable message archive or guarantee attachment delivery when media decryption is unavailable. ## Callback checks [#callback-checks] After startup, confirm the log contains `Chatwoot integration initialized`, then check the inbox webhook URL. It should be `https://wa.example.com/plugins/chatwoot/webhook?api_key=your-secure-key` when both `apiHost` and `apiKey` are set. Use Chatwoot's profile endpoint to check the token and account before diagnosing open-wa: ```bash curl -i \ -H "api_access_token: $CHATWOOT_API_ACCESS_TOKEN" \ "$CHATWOOT_ORIGIN/api/v1/profile" ``` A `200` response with an account ID confirms the token reaches Chatwoot. A `401` means the token or Chatwoot origin needs correction. Then send a direct WhatsApp message and a plain Chatwoot reply, checking the open-wa log and the callback response at each boundary. ## Development [#development] ```bash pnpm --filter @open-wa/integration-chatwoot dev ``` The plugin exports `chatwootPlugin`, `ChatwootClient`, `createChatwootRouter`, `ChatwootPluginConfig`, and `ChatwootConfig`. ## Documentation [#documentation] See the [Chatwoot guide](https://openwa.dev/docs/client-and-integrations/chatwoot) for the setup journey, media mapping, and recovery table. ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/integration-cloudflare (/docs/reference/workspaces/integrations/cloudflare) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/cloudflare/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/cloudflare/README.md) # @open-wa/cf-proxy [#open-wacf-proxy] `@open-wa/cf-proxy` is the Cloudflare Worker reverse tunnel for open-wa Easy API sessions. The local runtime opens an outbound WebSocket to the Worker, so the local machine does not need an inbound port. The Worker remains a remotely reachable authenticated entry point. The package is private to the open-wa monorepo. It is not an npm consumer package, so deployment starts from this repository's `packages/cf-proxy` entrypoint. ## Topology and authentication [#topology-and-authentication] ```text Remote consumer --HTTPS/WebSocket--> Cloudflare Worker --Durable Object--> local TunnelClient --HTTP--> Easy API ``` The Worker uses two independent secrets: * `UPSTREAM_TOKEN` authenticates the local runtime at `/sessions/:sessionId/upstream`. * `CONSUMER_TOKEN` authenticates remote callers on other session paths. The Worker routes each session ID to a Durable Object. An absent upstream returns `503 Session offline`; a request that waits more than 30 seconds returns `504 Gateway Timeout from session upstream`. Hibernation can reduce active Durable Object compute while the connection is idle, but it does not make Worker requests, storage, egress, domains, or account-plan charges free. Check [Cloudflare Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/) for the account and plan in use. ## Deploy the Worker [#deploy-the-worker] From a clean checkout: ```bash git clone https://github.com/open-wa/wa-automate-nodejs.git cd wa-automate-nodejs corepack enable pnpm install cd packages/cf-proxy pnpm exec wrangler login pnpm exec wrangler whoami ``` `wrangler.jsonc` binds the `SESSION_TUNNEL` Durable Object to `SessionTunnel`. Set the secret values through Wrangler without committing them: ```bash export OPENWA_PROXY_UPSTREAM_TOKEN="$(openssl rand -hex 32)" export OPENWA_PROXY_CONSUMER_TOKEN="$(openssl rand -hex 32)" printf '%s' "$OPENWA_PROXY_UPSTREAM_TOKEN" | pnpm exec wrangler secret put UPSTREAM_TOKEN printf '%s' "$OPENWA_PROXY_CONSUMER_TOKEN" | pnpm exec wrangler secret put CONSUMER_TOKEN pnpm exec wrangler deploy ``` Record the deployed hostname as `OPENWA_PROXY_HOST`, for example `https://open-wa-proxy..workers.dev`. The local runtime needs the upstream value; remote callers need the consumer value. Contributors with an existing checkout can run `pnpm install` from the repository root and use `pnpm --filter @open-wa/cf-proxy exec wrangler whoami` followed by `pnpm --filter @open-wa/cf-proxy exec wrangler deploy`. Run `secret put` again only when rotating a value. ## Attach a local session [#attach-a-local-session] Run the CLI on the machine that owns the WhatsApp browser session: ```bash export OPENWA_PROXY_HOST='https://open-wa-proxy..workers.dev' npx @open-wa/wa-automate@5.1.0 \ --session-id sales \ --port 8080 \ --proxy-host "$OPENWA_PROXY_HOST" \ --proxy-token "$OPENWA_PROXY_UPSTREAM_TOKEN" ``` The CLI converts the HTTPS host to a WebSocket host and connects to `/sessions/sales/upstream`. WhatsApp Web and Easy API stay on the local machine; the Worker does not run the browser or change session readiness. ## Consume a session [#consume-a-session] Use the consumer token in an `Authorization: Bearer` header rather than a URL query when making HTTP calls: ```bash curl -i \ "$OPENWA_PROXY_HOST/sessions/sales/api/getHostNumber" \ -H "Authorization: Bearer $OPENWA_PROXY_CONSUMER_TOKEN" ``` For a command that is protected by the local Easy API key, send both credentials: ```bash curl -i -X POST \ "$OPENWA_PROXY_HOST/sessions/sales/api/sendText" \ -H "Authorization: Bearer $OPENWA_PROXY_CONSUMER_TOKEN" \ -H "X-API-Key: $OPENWA_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"args":["1234567890@c.us","Hello through Cloudflare"]}' ``` Node clients can use the compatibility URL. The client converts it to the tunnel WebSocket and sends the consumer token to `/sessions//connect`: ```ts import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect( `cf-proxy://open-wa-proxy..workers.dev?sessionId=sales&token=${encodeURIComponent(process.env.OPENWA_PROXY_CONSUMER_TOKEN)}`, ); console.log(await client.getHostNumber()); ``` The remote caller still receives the local Easy API's authentication or readiness response. A valid consumer token does not replace the local `X-API-Key`. ## Troubleshooting [#troubleshooting] | Symptom | Command or observation | Expected result | Next action | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Worker deploy fails | `pnpm exec wrangler whoami` and inspect `wrangler.jsonc` | The intended account is authenticated and `SESSION_TUNNEL` binds `SessionTunnel`. | Log in to the intended account and deploy from `packages/cf-proxy`; fix binding or migration data before retrying. | | Local runtime gets `401 Unauthorized Upstream` | Compare `--proxy-token` with `pnpm exec wrangler secret list`. | The local runtime connects to the upstream WebSocket for the same session ID. | Rotate `UPSTREAM_TOKEN`, pass the exact value to the CLI, and restart it. | | Consumer gets `401 Unauthorized Consumer` | Repeat the HTTP request with the `Authorization` header above. | A matching consumer token reaches the Worker. | Rotate `CONSUMER_TOKEN` if necessary and reconnect callers. | | Consumer gets `503 Session offline` | Inspect the local runtime log for the upstream connection and match the session ID in the URL. | The upstream WebSocket is open for that session. | Start the local runtime or correct `--session-id`; then check local Easy API readiness. | | Consumer gets `504` | Call the same API path against `http://127.0.0.1:8080`. | The local API completes within 30 seconds. | Fix the local API, browser, or Easy API key before investigating Worker routing. | ## Development [#development] ```bash pnpm --filter @open-wa/cf-proxy dev ``` The Worker entrypoint is `packages/cf-proxy/src/index.ts`; the Durable Object is `src/tunnel-do.ts`. ## Documentation [#documentation] See the [Cloudflare Session Proxy guide](https://openwa.dev/docs/client-and-integrations/cf-proxy). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # Integrations (/docs/reference/workspaces/integrations) # Integrations [#integrations] The docs generator creates these pages from direct child README files under `integrations/`. * [@open-wa/integration-chatwoot](./chatwoot) - Chatwoot integration plugin for open-wa * [@open-wa/integration-cloudflare](./cloudflare) - Cloudflare Tunnel integration plugin for open-wa * [@open-wa/integration-jev](./jev) - Jev decision routing and WhatsApp chat actions, shared with Voxtral * [@open-wa/node-red](./node-red) - Official @open-wa node-red integration. * [@open-wa/integration-s3](./s3) - S3/Cloud storage integration plugin for open-wa * [@open-wa/integration-voxtral](./voxtral) - Voxtral conversational voice notes and live audio calls for open-wa * [@open-wa/integration-webhook](./webhook) - Webhook delivery integration plugin for open-wa # @open-wa/integration-jev (/docs/reference/workspaces/integrations/jev) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/jev/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/jev/README.md) # Jev for open-wa [#jev-for-open-wa] A chat assistant that chooses WhatsApp text, templates, reply buttons, lists, forms and booking cards. It works without Voxtral. Jev makes bounded decisions, Mistral composes wording, and open-wa owns state and executes the selected action. ```bash npm init @open-wa@latest chat-assistant -- --plugins jev cd chat-assistant ``` Choose Easy API or SDK and JavaScript or TypeScript. Copy `.env.example` to `.env`, set `MISTRAL_API_KEY` and `JEV_API_KEY`, then run `npm start`. The plugin is installed on demand. The [project builder](https://openwa.dev/new?plugins=jev) uses the same options. For an existing project, add `@open-wa/integration-jev` to `plugins` in `wa.config.js` or `wa.config.ts`. No separate install or config flag is needed. ```js export default { plugins: ['@open-wa/integration-jev'], pluginConfig: { jev: { agent: { instructions: ['Help people arrange demo appointments.', 'Send the requested details in this chat.'] }, conversation: { timeZone: 'Europe/London' }, }, }, }; ``` In Neo, open **Plugins → Jev** to edit settings. Normal **Chat** shows controls, OpenUI message previews, submitted/read states and expandable decision evidence. SDK mode runs the same assistant without an HTTP dashboard. Keys remain on the host; they are never included in settings responses. Add `@open-wa/integration-voxtral` for voice notes and calls. Voxtral depends on the shared Jev engine automatically; configuring the Jev plugin additionally enables plain text conversations. Both share context, booking state, pause controls and a send journal. Reply selections during a live call go to that call; other selections return to the adapter that sent the control, with text handling call selections after a call ends. Queued text waits until the call ends. Each channel can have its own agent instructions and model settings. Booking corrections preserve fields that Jev didn't identify as changed. The host copies selected literal times, phone numbers, URLs and references from source spans, and calculates dates in the agreed timezone. “Tomorrow” uses one captured turn instant. A bare weekday means its next occurrence on or after the reference day; “this” uses the current Monday-based week and “next” the following week. A missing year means the next occurrence of the supplied month/day. Nonexistent dates and ambiguous/nonexistent DST times require clarification. Unknown prose such as a new location still comes from Mistral and is reviewed by Jev. Incorrect transcripts can still produce incorrect facts. Interactive messages require the corresponding open-wa license. Without that capability, Jev can choose text and formatted text templates. A booking card is a chat message: it doesn't create or change a reservation in a booking system. The host sends a corrected card before attempting to delete its predecessor, and reports the actual removal result. Unknown sends aren't automatically retried. The action journal and settings are stored under `.open-wa/plugins/`; recent conversation context is in memory and expires after 30 minutes of inactivity by default. Restarting clears that context. # @open-wa/node-red (/docs/reference/workspaces/integrations/node-red) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/node-red/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/node-red/README.md) # @open-wa/node-red [#open-wanode-red] `@open-wa/node-red` connects Node-RED flows to an existing open-wa Easy API session. The current package is `5.1.0` and registers three node types: `owa-server`, `cmd`, and `listen`. The integration keeps the Node-RED palette small and maps commands and events to the Easy API metadata endpoints. It uses `@open-wa/socket-client` as the compatibility client; v5 commands travel over HTTP RPC and runtime events use the client event stream. ## Quick start [#quick-start] Start Easy API separately, then install the package into Node-RED's user directory: ```bash npx @open-wa/wa-automate@5.1.0 \ --session-id sales \ --port 8080 \ --api-key "$OPENWA_API_KEY" cd ~/.node-red npm install @open-wa/node-red@5.1.0 node-red ``` Open `http://127.0.0.1:1880`, choose **Manage palette**, and confirm `@open-wa/node-red` is installed. Add an **owa-server** configuration node with the Easy API URL and the same key passed to `--api-key`. Leave the key empty only when Easy API was started without one. The configuration node keeps the `SocketClient` in Node-RED global context. It reports `connected`, `disconnected`, and connection-error states so a flow can distinguish a stopped API from a command failure. ## Node types [#node-types] | Palette label | Runtime type | Behavior | | -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **owa-server** | `owa-server` | Stores `name`, `url`, and optional `key`, creates a `SocketClient`, and owns the connection lifecycle. | | **Command** | `cmd` | Loads command metadata from `/meta/basic/commands`, then calls `client.ask(method, args)` with node or message values. | | **Listen** | `listen` | Loads listener names from `/meta/basic/listeners`, subscribes with `client.listen(listener, callback)`, and emits event data as `msg.payload`. | The visible labels are **Command** and **Listen**. Older guides that describe separate Send, Receive, Session, or Media nodes refer to `@open-wa/node-red-contrib-wa-automate`, which is a different package and palette. ## Configuration fields [#configuration-fields] ### `owa-server` [#owa-server] * `name`: Node-RED display name. * `url`: Easy API base URL, such as `http://127.0.0.1:8080`. * `key`: Optional API key. The editor sends it as `X-API-Key` while loading command and listener metadata, and the client uses it for runtime requests. ### `cmd` [#cmd] * `server`: The **owa-server** configuration node. * `method`: Command name, such as `sendText`. * `args`: JSON arguments. Message values in `msg.method` and `msg.args` take precedence; object-like `msg.payload` is merged when configured arguments are object-like. * `timeout`: Seconds. Invalid or empty values fall back to 30 seconds; `-1` intentionally disables the command timeout. For `sendText`, the command arguments can be an object: ```json { "to": "1234567890@c.us", "content": "Hello from Node-RED" } ``` ### `listen` [#listen] * `server`: The **owa-server** configuration node. * `listener`: Listener name, for example `onMessage`. * `name`: Node-RED display name. The listener stops when its node closes or its server disconnects. A session that is not authenticated or ready cannot emit WhatsApp events even when Node-RED itself is running. ## Connection checks [#connection-checks] Use the same URL and key outside Node-RED to separate API reachability, authentication, and session readiness: ```bash curl -sS -D - \ -H "X-API-Key: $OPENWA_API_KEY" \ http://127.0.0.1:8080/health curl -sS -D - \ -H "X-API-Key: $OPENWA_API_KEY" \ http://127.0.0.1:8080/meta/basic/commands ``` A connection refusal means the URL or process is wrong. `401` means the configured key is absent or different. A `200` health response with `connected: false` or `session.ready: false` means Node-RED can reach Easy API but the WhatsApp session still needs authentication or readiness work. ## Development [#development] From the monorepo checkout: ```bash pnpm --filter @open-wa/node-red dev ``` The package source lives under `integrations/node-red`; the published Node-RED entrypoints are the `dist/nodes/*` files declared in `package.json`. ## Documentation [#documentation] See the [Node-RED guide](https://openwa.dev/docs/guides/node-red) for the importable echo flow and the full troubleshooting journey. ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/node-red/LICENSE.md). The [MIT notice](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/node-red/LICENSE) is retained for upstream Node-RED code. # @open-wa/integration-s3 (/docs/reference/workspaces/integrations/s3) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/s3/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/s3/README.md) # @open-wa/integration-s3 [#open-waintegration-s3] S3/Cloud storage integration plugin for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## What it does [#what-it-does] `@open-wa/integration-s3` handles `message.received` events that contain decryptable media. It uploads media through `pico-s3` to S3-compatible storage. It adds the resulting `cloudUrl` to the message object. The plugin assigns `cloudUrl` to the received message after asynchronous decryption and upload finish. It does not emit an upload-complete event, and other `message.received` listeners run independently, so they must not assume the field is present during that event callback. For an awaited completion result in application code, use the exported `S3Uploader.uploadMedia()` helper instead of the plugin hook. ## Configuration [#configuration] The configuration type is `S3Config` in `src/config.ts`. | Field | Necessary | Source-visible behavior | | ------------------- | --------- | ------------------------------------------------------------------------------------------- | | `provider` | Yes | Cloud provider identifier. Source type allows `aws`, `gcp`, `do`, `wasabi`, or `backblaze`. | | `accessKeyId` | Yes | Passed to `pico-s3` upload and URL generation options. | | `secretAccessKey` | Yes | Passed to `pico-s3` upload and URL generation options. | | `bucket` | Yes | Target bucket passed to `pico-s3`. | | `region` | No | Optional region passed to `pico-s3`. | | `public` | No | Optional public flag passed to `pico-s3`. | | `directory` | No | Optional directory strategy or literal directory string. | | `ignoreHostAccount` | No | When true, messages with `fromMe` are not uploaded. | | `headers` | No | Optional headers passed to upload options. | `directory` supports the exported `DirectoryStrategy` enum values `DATE`, `CHAT`, `DATE_CHAT`, and `CHAT_DATE`. A custom string is used directly as the upload directory. ## Runtime behavior [#runtime-behavior] * On `message.received`, the plugin checks for `deprecatedMms3Url`, `mimetype`, and a client with `decryptMedia`. * Matching media messages are passed to `S3Uploader.uploadMedia`. * `uploadMedia` skips messages without media URL or MIME type, and skips host-account messages when `ignoreHostAccount` is true. * File extensions are derived from the MIME type with the `mime` package, falling back to `bin`. * File names use `message.mId` when present, otherwise `Date.now()`. * Duplicate file names are tracked in memory and return the existing cloud URL instead of uploading again. * Uploads run through a bounded Effect queue with concurrency 2 and a two-per-second rate limit. * After a successful queue upload, `getCloudUrl` is used to compute the URL and the plugin assigns it to `message.cloudUrl`. * The mutation happens after `decryptMedia` and the queued upload resolve. There is no separate completion event for consumers. * Decryption or upload failures are logged and return `null`; the message is left without `cloudUrl` and no durable retry record is created. * On `dispose`, the plugin waits for the upload queue to become idle and logs that the queue drained. ## Exports [#exports] * `s3Plugin` from `src/plugin.ts`. * `S3Uploader` from `src/uploader.ts`; `uploadMedia(message, client)` resolves to a generated URL on success or `null` when there is no URL or upload fails. * `S3Config`, `CloudProvider`, and `DirectoryStrategy` from `src/config.ts`. `S3Uploader` accepts the same `S3Config` as `s3Plugin`. Its `client` argument supplies `decryptMedia(message)` as a base64 string. The high-level `Client.decryptMedia()` method returns a data URL, so an application adapting that facade should pass only the base64 portion to `uploadMedia`. `public` is passed through to `pico-s3`; it does not prove that a bucket or object is private, public, signed, or available for a particular duration. Provider-specific access and lifecycle behavior must be checked against the selected provider's configuration and policy. ## Development [#development] * `pnpm --filter @open-wa/integration-s3 build` * `pnpm --filter @open-wa/integration-s3 dev` * `pnpm --filter @open-wa/integration-s3 lint` * `pnpm --filter @open-wa/integration-s3 clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/integration-voxtral (/docs/reference/workspaces/integrations/voxtral) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/voxtral/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/voxtral/README.md) # Voxtral for open-wa [#voxtral-for-open-wa] Create a new voice assistant through the open-wa setup wizard: ```bash npm init @open-wa@latest voice-assistant -- --mode easy-api --plugins voxtral cd voice-assistant ``` Choose JS or TS, your browser, and Voice notes, Calls, or Both. Copy `.env.example` to `.env`, set `MISTRAL_API_KEY`, `JEV_API_KEY` for chat actions, and the calling license when needed, then run `npm start`. The [project builder](https://openwa.dev/new?plugins=voxtral) offers the same choices with file previews. The official Easy API plugin for conversational WhatsApp voice notes and live audio calls can also be added to an existing Easy API project. Set `MISTRAL_API_KEY` and add it to `wa.config.js` or `wa.config.ts`: ```js title="wa.config.js" export default { plugins: ['@open-wa/integration-voxtral'], }; ``` Easy API discovers `wa.config.js` or `wa.config.ts` automatically. Both accept `export default`; JavaScript also accepts `module.exports`. Run: ```bash npx @open-wa/wa-automate ``` The first start installs Voxtral on demand if it's missing, then reuses the project's `.open-wa/plugins/packages/` cache. You don't need a separate plugin install command. Only configured plugins are downloaded; Voxtral isn't bundled with Easy API. Open a person in Neo's normal Chat view for voice transcripts, replies, delivery states, call turns, and pause/takeover controls. Open `/dashboard/plugins/voxtral` to edit the assistant, choose Voice notes, Calls, or Both, preview a voice, and view usage. Its conversation list opens the same Chat feed. When someone asks during a call or voice note, Jev chooses the appropriate chat action: text, a confirmation template, buttons, a choice list, a form, or a native booking card. Set `JEV_API_KEY` on the Easy API host. Voxtral fills only the selected format and asks only for missing required details. Booking dates and displayed times are calculated by the plugin from the local date, time and timezone. Forms and buttons do not require booking details. To correct the latest card, say “No, I said 2 pm.” Voxtral keeps its other details, sends a corrected card, then removes the old one. If removal cannot be confirmed, it reports that the old card may still be visible. Corrections work in voice notes and calls while the card remains in the conversation context. Rich formats require an interactive-messaging license; Jev chooses among the formats available to the session. Templates are formatted text confirmations, not Meta-approved business templates. Chat actions are enabled by default; turn off **Edit assistant → Chat actions → Send messages when asked** or set `actions: { sendMessage: false }` inside `pluginConfig.voxtral`. **Choose format** previews Jev's decision without sending a WhatsApp message. Chat shows the chosen format, message details and actual submission/delivery state. Button, list and form replies to the assistant's own messages return to the conversation, through the active call or voice notes. In calls-only mode, selections outside a call are retained for the next call. Jev evaluates each user turn, proposed message content, action result and spoken reply. It receives recent conversation and actual submission outcomes, so it can advance an unfinished request, repair rejected arguments, retry a definitely-not-sent operation, ask for missing information or reconcile an uncertain result. A turn can build more than one requested message, such as a booking card followed by reply buttons. Accepted and uncertain sends are not repeated. The same loop runs for calls, voice notes and replies to interactive messages. The assistant waits for the send result and reply review before acknowledging it aloud. Sending booking details does not create a reservation or update a booking system. Without a Jev key, voice conversations continue but chat actions are unavailable. Voice notes work by default. Calling requires an open-wa calling-enabled license in the Easy API config. Audio encoding is included through `ffmpeg-static`; a system FFmpeg installation is not required. Instructions accept either a string or an array. The plugin handles joining, nested defaults, and environment-key lookup. Dashboard changes are saved as per-session overrides; reset them to return to your config file's settings. Credentials are never included in dashboard responses or saved overrides. Recent conversation text stays in memory for 30 minutes of inactivity by default. Raw audio and call recordings are not saved. Local settings, pause choices, bounded send journals, and usage counters live under `.open-wa/plugins/voxtral/`. Audio and conversation text used for responses go to your Mistral account. When chat actions are enabled, the latest request and a bounded excerpt of recent conversation, action results and proposed replies go to Jev for decisions and response review. The plugin page is an ES module supplied by this package. Neo supplies its React instance, BoardUI components, authenticated requests, privacy controls, and the shared call controller. Its `chat.js` module contributes to the existing Chat toolbar and timeline through the same host, with transcripts attached to their original WhatsApp messages where available. See the [guide](https://openwa.dev/docs/guides/voxtral) and [implementation contract](https://github.com/open-wa/wa-automate-nodejs/blob/master/docs/plans/2026-10-20-voxtral-integration.md). ## Text conversations with Jev [#text-conversations-with-jev] Voxtral uses the shared Jev decision and action engine. Add `@open-wa/integration-jev` to `plugins` to also answer ordinary text messages, or select Jev alongside Voxtral in the setup wizard. They share recent context, booking cards and send outcomes. During a live call, button and form replies return through call audio. See the [Jev guide](https://openwa.dev/docs/guides/jev) for setup, correction rules, dates and decision evidence. # @open-wa/integration-webhook (/docs/reference/workspaces/integrations/webhook) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`integrations/webhook/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/webhook/README.md) # @open-wa/integration-webhook [#open-waintegration-webhook] Webhook delivery integration plugin for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## What it does [#what-it-does] `@open-wa/integration-webhook` forwards public open-wa events to an external URL with HTTP `POST`. It supports event filtering, custom headers, request timeouts, retry with exponential backoff, and concurrent delivery through a queue. Use this integration to send open-wa runtime events to another service without direct open-wa integration. ## Configuration [#configuration] The plugin config is validated by the plugin SDK schema in `src/plugin.ts`. | Field | Necessary | Source-visible behavior | | --------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- | | `url` | Yes | Target URL for webhook delivery. Must be a URL. | | `events` | No | `all` or an array of event names. Defaults to `all`. Non-matching events are skipped. | | `concurrency` | No | Max concurrent deliveries. Defaults to 10. | | `queueCapacity` | No | Maximum queued deliveries in memory. Defaults to 1000. | | `overload` | No | A full queue waits for room with `backpressure` (default), or rejects with `dropping`. | | `retries` | No | Number of retry attempts after a failed delivery. Defaults to 3. | | `retryDelay` | No | Base retry delay in milliseconds. Defaults to 1000 and is exponentially backed off per attempt. | | `headers` | No | Additional headers merged into each request. | | `timeout` | No | Request timeout in milliseconds. Defaults to 30000. | | `durability` | No | When enabled, stores pending deliveries in a SQLite journal and replays up to `replayLimit` after startup. Disabled by default. | Each delivery also sends `Content-Type: application/json`. ## Payload envelope [#payload-envelope] The payload shape is defined by `WebhookPayload` in `src/config.ts` and produced in `src/plugin.ts`. | Field | Value | | ----------- | ---------------------------------------------------- | | `webhookId` | A random UUID generated when the plugin initializes. | | `sessionId` | The current open-wa session ID from the plugin host. | | `event` | The event name received by the plugin. | | `payload` | The event payload received by the plugin. | | `timestamp` | `Date.now()` at delivery enqueue time. | ## Runtime behavior [#runtime-behavior] * During initialization, the plugin creates a `WebhookDeliverer`, computes the allowed event set, and logs the configured target URL. * On each public `event` hook call, the plugin skips events outside the configured allowlist and enqueues the payload for delivery. * `WebhookDeliverer` posts JSON with native `fetch` and aborts requests with `AbortController` when the timeout elapses. * Non-2xx responses throw an error and enter the retry path. * Retry delay is `retryDelay * 2 ** attempt` until the configured retry count is exhausted. * The default queue and its waiting deliveries live in memory. A process exit or restart loses deliveries that were not accepted successfully. * After the final attempt, the deliverer logs the failure and the plugin hook rejects. With durability enabled, the delivery row is marked `dead_letter`; without it, no record remains for automatic replay. * With durability enabled, pending rows are replayed on a later startup up to `replayLimit`, and the sender includes the durable row ID as `Idempotency-Key`. Duplicate requests can still arrive if the receiver committed its work but the response was lost. * The plugin closes its delivery runtime on `dispose`; pending durable rows remain in SQLite for a later startup. The integration does not expose a built-in dead-letter inspection or replay route. Persist the receiver's inbox record or idempotency key before returning `2xx`. The sender does not create an HMAC signature, and its retry policy does not promise eventual delivery. Keep credentials out of logs and use a receiver-side secret or a signature scheme implemented by your gateway. ## Exports [#exports] * Default export and `webhookPlugin` from `src/plugin.ts`. * `WebhookDeliverer` from `src/deliverer.ts`. * `WebhookPluginConfig`, `WebhookConfig`, `Webhook`, and `WebhookPayload` types. ## Development [#development] * `pnpm --filter @open-wa/integration-webhook build` * `pnpm --filter @open-wa/integration-webhook dev` * `pnpm --filter @open-wa/integration-webhook lint` * `pnpm --filter @open-wa/integration-webhook clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/api (/docs/reference/workspaces/packages/api) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/api/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/api/README.md) # @open-wa/api [#open-waapi] Reusable Easy API transport and runtime explorer layer for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/api ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/cf-proxy (/docs/reference/workspaces/packages/cf-proxy) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/cf-proxy/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/cf-proxy/README.md) # @open-wa/cf-proxy [#open-wacf-proxy] Cloudflare Worker Reverse-Tunnel Proxy for open-wa Sessions Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/cf-proxy dev` * `pnpm --filter @open-wa/cf-proxy deploy` * `pnpm --filter @open-wa/cf-proxy typecheck` * `pnpm --filter @open-wa/cf-proxy build` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/cli (/docs/reference/workspaces/packages/cli) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/cli/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/cli/README.md) # @open-wa/cli [#open-wacli] `@open-wa/cli` provides the `wa` command for starting and managing an Open-WA session and API server. The CLI delegates session startup to `@open-wa/wa-automate`. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash npm install --global @open-wa/cli@5.1.0 ``` ## Start the API server [#start-the-api-server] ```bash wa --session-id main --host 127.0.0.1 --port 8080 ``` Scan the QR code shown in the terminal to sign in. The server listens on `127.0.0.1:8080`; use `wa --help` to see the available options. Add an API key with `--api-key` when clients need authentication, and keep the server on loopback or a private network unless you have configured an appropriate access boundary. ## Remote clients [#remote-clients] Use [`@open-wa/socket-client`](https://openwa.dev/docs/client-and-integrations/socket-client) to send commands to this server and receive runtime events. ## Documentation [#documentation] See the [CLI reference](https://openwa.dev/docs/guides/configuration-and-cli). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/client (/docs/reference/workspaces/packages/client) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/client/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/client/README.md) # @open-wa/client [#open-waclient] `@open-wa/client` provides the in-process TypeScript client for a WhatsApp Web session. It wraps the client created by `@open-wa/core`. For a remote HTTP server, use `@open-wa/socket-client`. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash bun add @open-wa/client @open-wa/core @open-wa/driver-puppeteer ``` ## Start a session and send a message [#start-a-session-and-send-a-message] ```typescript import { Client } from '@open-wa/client'; import { createClient } from '@open-wa/core'; import { PuppeteerDriver } from '@open-wa/driver-puppeteer'; const session = await createClient({ sessionId: 'main-session', driver: new PuppeteerDriver(), }); const client = new Client({ client: session, transport: session.getTransport() }); await client.start(); await client.sendText('447700900123@c.us', 'Hello from Open-WA'); await client.stop(); ``` The client starts the browser session in your process. See the [custom code guide](https://openwa.dev/docs/getting-started/custom-code) for authentication and browser-profile setup. ## Text send results [#text-send-results] `sendText(chatId, content)` resolves to a validated serialized `MessageId` string, including when the runtime returns an object containing `_serialized`. A returned ID is not a delivery or read receipt. Failures throw the exported `SendTextError`, whose `code` explains the failure and whose `outcome` distinguishes a rejected send from an uncertain attempt. | Code | Outcome | Meaning | | --------------------- | ---------- | ---------------------------------------------------------------------------------------------- | | `INVALID_ARGUMENT` | `not_sent` | The chat ID or text is missing or is not a string; no browser call was made. | | `LICENSE_REQUIRED` | `not_sent` | Starting this chat requires an applied restricted or premium license. | | `SEND_REJECTED` | `not_sent` | The runtime rejected the unsupported broadcast recipient. | | `INVALID_SEND_RESULT` | `unknown` | The runtime returned no usable message ID, including `false` or an unrecognized failure value. | | `SEND_FAILED` | `unknown` | Browser evaluation failed; the original error is available as `cause`. | For an `unknown` outcome, check the chat before deciding whether to send again. The client never retries automatically because the first attempt may already have sent the message. ## Media and patch-backed messages [#media-and-patch-backed-messages] Media methods accept a data URL, base64 string, URL, local path or `Buffer`. `sendFile` uses the detected media type, so a video appears as a video and an audio file appears as audio. It does not force every file into a document attachment. ```typescript await client.sendFile(chatId, './clip.mp4', 'clip.mp4', { caption: 'Watch this' }); await client.sendAudio(chatId, './sound.mp3', 'sound.mp3'); await client.sendPtt(chatId, './voice.ogg', { viewOnce: true }); await client.sendFile(chatId, './note.mp4', 'note.mp4', { type: 'video', videoNote: true }); await client.sendVideoAsGif(chatId, './loop.mp4', 'loop.mp4'); await client.sendFileFromUrl(chatId, downloadUrl, 'clip.mp4', {}, { Authorization: token }); await client.sendPoll(chatId, 'Which day?', ['Friday', 'Saturday', 'Sunday'], { selectableCount: 2 }); await client.pinMessage(messageId, 'SevenDays'); await client.react(messageId, '👍'); await client.editMessage(messageId, 'Updated text'); ``` `sendPtt` marks prepared audio as a voice note; it does not transcode the source. `sendVideoAsGif` accepts video and enables WhatsApp's looping GIF presentation. Media and poll sends return a serialized message ID or a boolean submission result. Neither confirms recipient delivery; an uncertain result should be checked in the chat before retrying. `sendVCard(chatId, vcard, contactName?, contactNumber?)` sends supplied vCard content. `sendMultipleContacts(chatId, contactIds)` sends one contact bundle, while `sendContact` sends each supplied contact separately and returns the individual results. `postTextStatus(text, backgroundColor?, font?)`, `postImageStatus(image, caption?)` and `postVideoStatus(video, caption?)` use the corresponding licensed story payloads. Text backgrounds accept RGB or RGBA hex colors, and fonts are numbered 0–5. Missing common or licensed methods produce an error when called; these methods require the current patch delivery. ## Documentation [#documentation] See the [custom code guide](https://openwa.dev/docs/getting-started/custom-code) for setup and [how Open-WA works](https://openwa.dev/docs/concepts/how-it-works) for the package roles. ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm Media settings use the shared `MediaSendOptions` contract. Set `type: 'video'` for `isGif` or `videoNote`, `type: 'audio'` for `ptt`, or `type: 'document'` to force a document attachment. Circular notes require prepared square MP4 video of at most 60 seconds. See the media guide for allowed combinations. Polls expose `voteOnPoll`, `getPollResults` and `onPollVote`; optional `endTime` uses Unix milliseconds and `hideVoterNames` requires native sending-account support. # @open-wa/config (/docs/reference/workspaces/packages/config) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/config/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/config/README.md) # @open-wa/config [#open-waconfig] Configuration management for open-wa - schemas, loading, merging, and validation Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/config ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/core (/docs/reference/workspaces/packages/core) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/core/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/core/README.md) # @open-wa/core [#open-wacore] `@open-wa/core` creates and runs a browser-backed Open-WA session. It owns session lifecycle, events, plugins, readiness state, and the transport used by `@open-wa/client` to call WhatsApp Web methods. Applications can usually start with `@open-wa/wa-automate`, which assembles these packages behind a CLI and HTTP API. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Main exports [#main-exports] * `createClient(options)` creates a session and returns an `OpenWAClient` with `start`, `stop`, `getState`, `getReadiness`, and `getTransport` methods. * `OpenWAClient.events` exposes the session event emitter. * Plugin, session, and transport types are also exported from this package. The `driver` option is required. See the [custom code guide](https://openwa.dev/docs/getting-started/custom-code) for an example using `@open-wa/driver-puppeteer`, and [how Open-WA works](https://openwa.dev/docs/concepts/how-it-works) for how the packages fit together. ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/create (/docs/reference/workspaces/packages/create) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/create/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/create/README.md) # Create an open-wa project [#create-an-open-wa-project] ```bash npm init @open-wa@latest my-bot cd my-bot npm start ``` Use Node.js 22.21.1 or newer. The wizard lets you choose Easy API or SDK, JavaScript or TypeScript, a browser, and optional Voxtral, Webhook or Chatwoot plugins. Missing choices are interactive; `--yes` uses defaults. Configure the same options in the [project builder](https://openwa.dev/new) to preview the exact generated files and copy a repeatable command: ```bash npm init @open-wa@latest voice-bot -- --mode easy-api --language ts --browser chrome --plugins voxtral --voxtral-mode both --yes ``` Copy `.env.example` to `.env` when it is generated and fill in the selected integrations' credentials. Browser and plugin downloads happen only when needed at startup and are cached for reuse. The initializer never overwrites existing destination files. `--no-install` generates the project without running npm. The CLI, builder and file preview share `src/options.js` and `src/templates.js`. The existing `wa-automate init` command delegates to this package. See the [setup guide](https://openwa.dev/docs/getting-started/initializer) for all flags and generated files. # @open-wa/decrypt (/docs/reference/workspaces/packages/decrypt) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/decrypt/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/decrypt/README.md) # @open-wa/decrypt [#open-wadecrypt] Lightweight WhatsApp media decryption — zero external dependencies Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/decrypt ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/domain (/docs/reference/workspaces/packages/domain) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/domain/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/domain/README.md) # @open-wa/domain [#open-wadomain] Domain data structures for open-wa WhatsApp automation (Collectors, etc.) Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/domain ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/driver-interface (/docs/reference/workspaces/packages/driver-interface) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/driver-interface/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/driver-interface/README.md) # @open-wa/driver-interface [#open-wadriver-interface] Browser driver abstraction interfaces for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/driver-interface ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/driver-lightpanda (/docs/reference/workspaces/packages/driver-lightpanda) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/driver-lightpanda/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/driver-lightpanda/README.md) # @open-wa/driver-lightpanda [#open-wadriver-lightpanda] Lightpanda browser driver for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/driver-lightpanda ``` ## Documentation [#documentation] The driver uses `puppeteer-core`, so package installation does not download a browser. Launch downloads Lightpanda 1.0.0 only when needed, verifies its release SHA-256 digest, and reuses it from `~/.cache/open-wa/lightpanda`. A custom `lightpanda.executablePath` or `LIGHTPANDA_EXECUTABLE_PATH` takes precedence. Basic mode also detects an installed binary on `PATH` or in `~/.local/bin`; experimental WhatsApp mode uses the pinned release for its prepared cached copy. Set `browser.download: 'never'` to require an existing binary, or `browser.cacheDirectory` to choose another cache. Lightpanda provides lightweight basic automation with experimental WhatsApp support. It has low compatibility and no rendering, screenshots or video. Use Chrome for the widest media and browser compatibility. Windows users must run Lightpanda inside WSL2. See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/driver-playwright (/docs/reference/workspaces/packages/driver-playwright) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/driver-playwright/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/driver-playwright/README.md) # @open-wa/driver-playwright [#open-wadriver-playwright] Playwright driver implementation for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/driver-playwright ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/driver-puppeteer (/docs/reference/workspaces/packages/driver-puppeteer) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/driver-puppeteer/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/driver-puppeteer/README.md) # @open-wa/driver-puppeteer [#open-wadriver-puppeteer] Puppeteer driver implementation for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash bun add @open-wa/driver-puppeteer ``` ## Browser setup [#browser-setup] Installing the driver installs `puppeteer-core` without downloading a browser. Startup reuses cached Chrome, or downloads the Puppeteer-matched Chrome for Testing with progress when it is missing. Set `browser.kind: 'chromium'` to download and reuse Chromium instead. Downloads use `PUPPETEER_CACHE_DIR` or `~/.cache/puppeteer`. Concurrent open-wa starts share one installation; an interrupted download is retried on the next start. Use the same resolver to pre-provision a deployment cache: ```typescript import { ensureBrowser } from '@open-wa/driver-puppeteer'; const executablePath = await ensureBrowser({ browser: { cacheDirectory: '/app/browser-cache', download: 'auto' }, }); console.log(executablePath); ``` Provision on the same operating system and architecture as the deployment. Then pass `browser: { cacheDirectory: '/app/browser-cache', download: 'never' }` to `create()` or the driver's `launch()` to forbid downloads at runtime. An explicit `download` option takes precedence over `PUPPETEER_SKIP_DOWNLOAD` and `PUPPETEER_CHROME_SKIP_DOWNLOAD`; otherwise those environment settings are respected. `executablePath` overrides managed Chrome. A missing or non-executable path produces an error, so a typo cannot silently select a different browser. `PUPPETEER_EXECUTABLE_PATH` is also respected. Downloads honor `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`; `browser.downloadBaseUrl` selects a Chrome for Testing mirror. Chrome's sandbox remains enabled by default. open-wa does not install operating system packages. If Chrome reports a missing shared library or unavailable sandbox, configure the host or deployment image using [Puppeteer's troubleshooting guide](https://pptr.dev/troubleshooting). ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/hyperemitter (/docs/reference/workspaces/packages/hyperemitter) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/hyperemitter/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/hyperemitter/README.md) # @open-wa/hyperemitter [#open-wahyperemitter] `@open-wa/hyperemitter` is a typed event emitter with direct synchronous dispatch and Effect-owned asynchronous work. It supports exact event names, `+` and trailing `#` wildcard subscriptions, one-time listeners, catch-all listeners, and listener cleanup with an `AbortSignal` or an Effect scope. Listeners can return a Promise or an Effect. ## Use [#use] ```typescript import { HyperEmitter } from '@open-wa/hyperemitter'; type Events = { 'message.received': { id: string; body: string }; 'session.state.changed': { state: string }; }; const events = new HyperEmitter(); events.on('message.received', (message) => { console.log(message.id, message.body); }); events.emit('message.received', { id: 'message-id', body: 'Hello' }); ``` `emit` invokes ordinary callbacks directly, entering Effect only for asynchronous listener work in fibers owned by the emitter. Callback-only use creates no PubSub. `emitEffect` batches callbacks in a lazy Effect, awaits every listener, and exposes uncaptured failures in the Effect error channel: ```typescript import { Effect } from 'effect'; await Effect.runPromise(Effect.scoped(Effect.gen(function* () { yield* events.onScoped('message.received', (message) => Effect.sync(() => console.log(message.body)), ); yield* events.emitEffect('message.received', { id: 'message-id', body: 'Hello' }); }))); ``` `onScoped` and `onAnyScoped` remove their registrations and interrupt their listener fibers when the caller's scope closes. `Effect.runPromise(events.close())` removes all registrations and interrupts work owned by the emitter. A closed emitter cannot accept new subscriptions. Each event carries one payload. `+` matches one name segment; a trailing `#` matches the rest, including an empty suffix. Set `captureRejections: true` to report listener failures through `onError` or the emitter's logger while continuing delivery. The [Effect benchmark](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/hyperemitter/benchmarks/README.md) compares callback dispatch, Effect dispatch, asynchronous listeners, and scope cleanup on completed deliveries. Its results are measurements from that runner, not a performance guarantee. ## License [#license] Apache-2.0 ## Shared OpenWA emitter [#shared-openwa-emitter] The exported `EventEmitter` backs OpenWA's global `ev` and the socket clients' `ev` properties. It preserves the callback API: variadic arguments, synchronous `emit`, `*` and `**` namespace wildcards, `this.event`, listener ordering and removal, `emitAsync` result arrays, `waitFor`, and `listenTo`. It adapts that surface to the same tuned HyperEmitter rather than creating a second runtime. `onScoped`, `onAnyScoped`, `emitEffect`, and `close` add Effect ownership. The [PubSub comparison](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/hyperemitter/benchmarks/results/pubsub-node-2026-10-14.json) measures completed deliveries with both per-event acknowledgements and batches of 256, including consumers that drain queued messages in batches. PubSub is a useful asynchronous stream option, but publishing does not guarantee immediate callback invocation. A busy subscriber queues later events, which differs from the shared emitter's dispatch contract. ## Optional PubSub and Streams [#optional-pubsub-and-streams] Both `HyperEmitter` and the shared `EventEmitter` facade own a lazy native Effect PubSub. Calling `pubsub()`, running `subscribe()`, or consuming `stream()` enables queued observation. Ordinary callback users create no PubSub or persistent consumer fibers. ```ts import { Effect, PubSub } from 'effect'; import { ev } from '@open-wa/wa-automate'; await Effect.runPromise(Effect.scoped(Effect.gen(function* () { // Direct native access; ev.subscribe() is the equivalent convenience API. const subscription = yield* PubSub.subscribe(ev.pubsub()); ev.emit('app.job.finished', { jobId: 'daily-summary' }); const { event, args } = yield* PubSub.take(subscription); console.log(event, args[0]); }))); ``` The public facade publishes `{ event, args }`; typed `HyperEmitter` publishes `{ event, payload }`, preserving the relationship between event names and payload types. Envelopes and argument arrays are immutable; payload objects keep their original identity. Observations are published before callbacks, so a recursive emission preserves the original queued order. They describe emissions, including ones whose callbacks later fail. `emit`, `emitEffect`, and `emitAsync` deliver through both interfaces once. `emit` still returns whether callback listeners ran. `emitAsync` returns those callbacks' results and does not wait for arbitrary PubSub consumers. Raw `PubSub.publish(emitter.pubsub(), frame)` addresses the queued feed directly; use emitter methods when callbacks should also run. The owning emitter shuts the hub down on `close()`, interrupting queued readers. Consumer scopes remove their own subscriptions. Closing does not promise to drain buffered work. The default is unbounded with replay disabled. A slow active observer can retain a growing backlog. Choose options on first access for an explicit finite loss policy, or retain recent emissions for later subscribers: ```ts import { EventEmitter } from '@open-wa/hyperemitter'; const emitter = new EventEmitter(); const hub = emitter.pubsub({ strategy: 'sliding', capacity: 256, replay: 1 }); // Later calls return the same hub; options cannot silently replace it. ``` `dropping` rejects new observations when full; `sliding` removes older ones. `pubsubStatus` counts accepted and rejected emitter publications separately from callback results; raw publication bypasses these counters. Sliding accepts the new publication, so its rejected count does not count evicted older frames. Lossless bounded backpressure cannot sit transparently beneath synchronous `emit`, so observation options offer only non-blocking strategies. Use an integration-owned Queue and effectful producer when producer suspension is part of that integration's design. `stream()` supplies the same observations through `Stream.fromPubSub`, with normal Effect filtering, batching, and consumer concurrency controls. Each stream starts its subscription when consumed; subscribe before emitting when replay is disabled. The global `ev` feed forwards native events and enabled session aliases even when no callback is registered. Session shutdown detaches its bridge without closing the shared global hub. See the [source-backed architecture review](https://github.com/open-wa/wa-automate-nodejs/blob/master/docs/research/2026-10-14-effect-event-system-designs.md) for the responsibilities of PubSub, Queue, Stream, SubscriptionRef, services, and scoped fibers. The [Effect event guide](https://openwa.dev/docs/guides/effect-event-streams) includes complete examples; the [HyperEmitter reference](https://openwa.dev/docs/reference/hyperemitter) documents method returns and lifecycle semantics. The Effect hybrid and optional PubSub APIs are available from OpenWA 5.6.0. # Packages (/docs/reference/workspaces/packages) # Packages [#packages] The docs generator creates these pages from direct child README files under `packages/`. * [@open-wa/api](./api) - Reusable Easy API transport and runtime explorer layer for open-wa * [@open-wa/cf-proxy](./cf-proxy) - Cloudflare Worker Reverse-Tunnel Proxy for Open-WA Sessions * [@open-wa/cli](./cli) - CLI for @open-wa WhatsApp automation - Easy API mode * [@open-wa/client](./client) - User-facing API client for open-wa WhatsApp automation * [@open-wa/config](./config) - Configuration management for open-wa - schemas, loading, merging, and validation * [@open-wa/core](./core) - Core orchestration backbone for open-wa WhatsApp automation * [@open-wa/create](./create) - Create an open-wa Easy API or SDK project * [@open-wa/decrypt](./decrypt) - Lightweight WhatsApp media decryption — zero external dependencies * [@open-wa/domain](./domain) - Domain data structures for open-wa WhatsApp automation (Collectors, etc.) * [@open-wa/driver-interface](./driver-interface) - Browser driver abstraction interfaces for open-wa * [@open-wa/driver-lightpanda](./driver-lightpanda) - Lightpanda browser driver for open-wa * [@open-wa/driver-playwright](./driver-playwright) - Playwright driver implementation for open-wa * [@open-wa/driver-puppeteer](./driver-puppeteer) - Puppeteer driver implementation for open-wa * [@open-wa/hyperemitter](./hyperemitter) - HyperEmitter: high-performance, schema-driven event emitter for the open-wa v5 stack. * [@open-wa/logger](./logger) - Centralized logging and terminal UI for open-wa v5 * [@open-wa/mcp](./mcp) - MCP server integration for open-wa * [orchestrator](./orchestrator) - orchestrator workspace in the open-wa v5 monorepo. * [@open-wa/plugin-sdk](./plugin-sdk) - SDK for building open-wa plugins — types, helpers, and the createPlugin() factory * [@open-wa/runtime-browser](./runtime-browser) - Browser Effect runtime Layer for open-wa control-plane clients * [@open-wa/runtime-bun](./runtime-bun) - Bun Effect runtime Layer for open-wa * [@open-wa/runtime-core](./runtime-core) - Portable Effect runtime capabilities for open-wa * [@open-wa/runtime-edge](./runtime-edge) - Web API Effect runtime Layer for open-wa edge control planes * [@open-wa/runtime-node](./runtime-node) - Node Effect runtime Layer for open-wa * [@open-wa/schema](./schema) - Zod-based schema definitions for open-wa * [@open-wa/screencaster](./screencaster) - Headless browser screencasting for open-wa — treeshakable server + client * [@open-wa/session-sync](./session-sync) - Synchronize Open-WA sessions to remote storage * [@open-wa/socket-client](./socket-client) - Easily connect to remote instances of the EASY API * [@open-wa/stickers](./stickers) - Local sticker rendering with automatic browser or standalone WASM execution * [@open-wa/wa-automate-types-only](./types-only) - Lightweight type-only re-exports from @open-wa/schema — zero runtime dependencies * [@open-wa/ui-components](./ui-components) - Reusable React UI components for open-wa web applications * [@open-wa/utils](./utils) - Shared utility functions for open-wa * [@open-wa/wa-automate](./wa-automate) - WhatsApp automation CLI and API server # @open-wa/logger (/docs/reference/workspaces/packages/logger) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/logger/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/logger/README.md) # @open-wa/logger [#open-walogger] Centralized logging and terminal UI for open-wa v5 Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/logger ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/mcp (/docs/reference/workspaces/packages/mcp) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/mcp/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/mcp/README.md) # @open-wa/mcp [#open-wamcp] MCP server integration for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/mcp ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). Hosted MCP runs on Easy API with an API key and `mcp.enabled: true`. It serves MCP 2.0 (`2026-07-28`), with tools and webhook event subscriptions on the same endpoint. Clients must support that protocol revision. ChatGPT Work can subscribe to `message.received`, `ack.changed` and `session.state.changed`. Subscriptions and pending deliveries persist under `.open-wa/`; keep this directory on persistent storage. Callbacks are verified, signed using Standard Webhooks and restricted to public HTTPS destinations. There is no historical event replay (`cursor: null`). See [MCP Events](https://openwa.dev/docs/client-and-integrations/mcp-events) and the [event contracts](https://openwa.dev/docs/reference/event-contracts). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # orchestrator (/docs/reference/workspaces/packages/orchestrator) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/orchestrator/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/orchestrator/README.md) # orchestrator [#orchestrator] orchestrator workspace in the open-wa v5 monorepo. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * This package defines no package-level scripts. ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/plugin-sdk (/docs/reference/workspaces/packages/plugin-sdk) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/plugin-sdk/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/plugin-sdk/README.md) # @open-wa/plugin-sdk [#open-waplugin-sdk] SDK for building open-wa plugins — types, helpers, and the `createPlugin()` factory. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). A plugin runs inside the open-wa runtime and can listen to WhatsApp events, call WhatsApp methods through a transport-agnostic client, expose HTTP routes and dashboard pages, and register AI tools — all without owning the browser or session lifecycle yourself. ## Install [#install] ```bash pnpm add @open-wa/plugin-sdk ``` ## Minimal plugin [#minimal-plugin] ```ts import { createPlugin } from '@open-wa/plugin-sdk'; function getTextMessage(message: unknown) { if (!message || typeof message !== 'object') return null; const candidate = message as { body?: unknown; from?: unknown }; if (typeof candidate.body !== 'string' || typeof candidate.from !== 'string') { return null; } return { body: candidate.body, from: candidate.from }; } export default createPlugin({ meta: { name: 'greeting-bot', version: '1.0.0', description: 'Greets contacts with a welcome message', }, init: async ({ client, logger }) => ({ 'message.received': async ({ message }) => { const msg = getTextMessage(message); if (!msg) return; if (msg.body === 'Hi') { await client.sendText(msg.from, '👋 Welcome! How can I help?'); logger.info('Sent greeting', { from: msg.from }); } }, }), }); ``` ## Typed config [#typed-config] Pass a Zod schema as `configSchema`; it is validated before `init()` runs and the parsed value is passed as `config`. `z` is re-exported so you don't need to install Zod separately. ```ts import { createPlugin, z } from '@open-wa/plugin-sdk'; const configSchema = z.object({ greeting: z.string().default('👋 Welcome!'), }); export default createPlugin>({ meta: { name: 'greeting-bot', version: '1.0.0' }, configSchema, init: async ({ config, client }) => ({ 'message.received': async ({ message }) => { // config.greeting is typed and validated }, }), }); ``` `defineConfig()` is also available as a factory helper when you prefer it: ```ts import { defineConfig } from '@open-wa/plugin-sdk'; export const config = defineConfig((z) => z.object({ greeting: z.string().default('👋 Welcome!') }), ); ``` ## Export requirements [#export-requirements] The runtime loads a plugin from a module's **default export** or a named `plugin` export. Both work: ```ts export default createPlugin({ /* ... */ }); // or export const plugin = createPlugin({ /* ... */ }); ``` ## Loading a plugin [#loading-a-plugin] Plugins are referenced by npm package name, scoped name, or file path, and configured under `pluginConfig` keyed by the plugin's `meta.name` (not the package name): ```ts export default { plugins: ['greeting-bot', './my-local-plugin'], pluginConfig: { 'greeting-bot': { greeting: 'Hello!' }, }, }; ``` ## Documentation [#documentation] Full authoring guide, hooks reference, security model, publishing, and worked examples are on the docs site: * [Getting started with plugins](https://openwa.dev/docs/plugins/getting-started) * [Hooks reference](https://openwa.dev/docs/plugins/hooks-reference) * [Plugin client](https://openwa.dev/docs/plugins/plugin-client) * [Security model](https://openwa.dev/docs/plugins/security-model) * [Publishing a plugin](https://openwa.dev/docs/plugins/publishing) Real reference plugins in this repo: * [`integrations/webhook/src/plugin.ts`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/webhook/src/plugin.ts) * [`integrations/chatwoot/src/plugin.ts`](https://github.com/open-wa/wa-automate-nodejs/blob/master/integrations/chatwoot/src/plugin.ts) ## OpenUI messages [#openui-messages] `@open-wa/plugin-sdk/openui` supplies a shared component catalogue, agent prompts, `createOpenUIMessage`, `compileOpenUI`, `sendOpenUI` and response validation. Neo supplies its renderer as `host.openui.Message` to plugin pages and Chat extensions. See the [OpenUI guide](https://openwa.dev/docs/guides/openui). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/runtime-browser (/docs/reference/workspaces/packages/runtime-browser) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/runtime-browser/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/runtime-browser/README.md) # @open-wa/runtime-browser [#open-waruntime-browser] Browser Effect runtime Layer for open-wa control-plane clients Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/runtime-browser build` * `pnpm --filter @open-wa/runtime-browser test` * `pnpm --filter @open-wa/runtime-browser lint` * `pnpm --filter @open-wa/runtime-browser typecheck` * `pnpm --filter @open-wa/runtime-browser clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/runtime-bun (/docs/reference/workspaces/packages/runtime-bun) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/runtime-bun/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/runtime-bun/README.md) # @open-wa/runtime-bun [#open-waruntime-bun] Bun Effect runtime Layer for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/runtime-bun build` * `pnpm --filter @open-wa/runtime-bun test` * `pnpm --filter @open-wa/runtime-bun lint` * `pnpm --filter @open-wa/runtime-bun typecheck` * `pnpm --filter @open-wa/runtime-bun clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/runtime-core (/docs/reference/workspaces/packages/runtime-core) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/runtime-core/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/runtime-core/README.md) # @open-wa/runtime-core [#open-waruntime-core] Portable Effect runtime capabilities for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/runtime-core build` * `pnpm --filter @open-wa/runtime-core test` * `pnpm --filter @open-wa/runtime-core lint` * `pnpm --filter @open-wa/runtime-core typecheck` * `pnpm --filter @open-wa/runtime-core clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/runtime-edge (/docs/reference/workspaces/packages/runtime-edge) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/runtime-edge/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/runtime-edge/README.md) # @open-wa/runtime-edge [#open-waruntime-edge] Web API Effect runtime Layer for open-wa edge control planes Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/runtime-edge build` * `pnpm --filter @open-wa/runtime-edge test` * `pnpm --filter @open-wa/runtime-edge lint` * `pnpm --filter @open-wa/runtime-edge typecheck` * `pnpm --filter @open-wa/runtime-edge clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/runtime-node (/docs/reference/workspaces/packages/runtime-node) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/runtime-node/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/runtime-node/README.md) # @open-wa/runtime-node [#open-waruntime-node] Node Effect runtime Layer for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Development [#development] * `pnpm --filter @open-wa/runtime-node build` * `pnpm --filter @open-wa/runtime-node test` * `pnpm --filter @open-wa/runtime-node lint` * `pnpm --filter @open-wa/runtime-node typecheck` * `pnpm --filter @open-wa/runtime-node clean` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/schema (/docs/reference/workspaces/packages/schema) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/schema/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/schema/README.md) # @open-wa/schema [#open-waschema] Zod-based schema definitions for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/schema ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/screencaster (/docs/reference/workspaces/packages/screencaster) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/screencaster/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/screencaster/README.md) # @open-wa/screencaster [#open-wascreencaster] Headless browser screencasting for open-wa — treeshakable server + client Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/screencaster ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/session-sync (/docs/reference/workspaces/packages/session-sync) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/session-sync/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/session-sync/README.md) # @open-wa/session-sync [#open-wasession-sync] Synchronize open-wa sessions to remote storage (S3/Zstd). Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/session-sync ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/socket-client (/docs/reference/workspaces/packages/socket-client) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/socket-client/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/socket-client/README.md) # @open-wa/socket-client [#open-wasocket-client] Use `@open-wa/socket-client` from a Node.js application to call a running `@open-wa/wa-automate` API server and receive its runtime events. Commands use the server's HTTP API, and event subscriptions use Server-Sent Events (SSE). ## Start the API server [#start-the-api-server] ```bash npx @open-wa/wa-automate@5.1.0 --session-id main --host 127.0.0.1 --port 8002 --api-key replace-this-key ``` Keep the server on loopback or a private network unless you have configured an appropriate access boundary. The API key is sent by the client in the `X-API-Key` header for commands and as an `api_key` query parameter for the SSE connection. ## Connect and send a message [#connect-and-send-a-message] ```bash npm install @open-wa/socket-client@5.1.0 ``` ```typescript import { SocketClient } from '@open-wa/socket-client'; const client = await SocketClient.connect('http://127.0.0.1:8002', 'replace-this-key'); client.onAnyMessage((message) => { console.log(message.id, message.body); }); await client.sendText('447700900123@c.us', 'Hello from a remote client'); client.close(); ``` `SocketClient.connect` waits for the event stream to connect before returning. The client provides typed convenience methods for the API, along with `ask` for calling a method by name. ## Documentation [#documentation] See the [socket client guide](https://openwa.dev/docs/client-and-integrations/socket-client) for setup and available methods. ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm # @open-wa/stickers (/docs/reference/workspaces/packages/stickers) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/stickers/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/stickers/README.md) # @open-wa/stickers [#open-wastickers] Create WebP stickers locally, using an available browser for jobs it can execute completely, or a portable standalone worker for the whole job. No Sharp, native addon build, browser download or default model weights. ```js import { createSticker } from '@open-wa/stickers'; import { writeFile } from 'node:fs/promises'; const sticker = await createSticker('./photo.jpg', { author: 'My label', pack: 'My pack', effects: ['comic', { name: 'tint', options: { color: '#e056ae' } }, 'sparkle'], }); await writeFile('./sticker.webp', sticker.bytes); // The SDK supplies its existing browser context automatically. await client.sendSticker(to, './photo.jpg', { effects: ['comic', 'sparkle'], author: 'My label', pack: 'My pack', quotedMsgId: messageId, // optional sticker reply }); ``` The SDK imports this package on the first sticker request. Rendering produces pixels and optional author/pack labels. Sending requires the private sticker patch: it owns donor app links and finalizes EXIF before native hashing, encryption and upload. Rendering alone doesn't produce donor-branded output. Sticker payloads are rejected by `sendRawMessage`; use `sendSticker` to reach the finalizer. Native sticker forwarding is blocked until its local re-finalization path is ported. ## Routing and lifecycle [#routing-and-lifecycle] `createSticker()` manages its own runtime. Call it directly, including for bursts; there is no runtime to initialize or dispose: ```js const stickers = await Promise.all(files.map(file => createSticker(file, { author: 'My label', pack: 'My pack', effects: ['comic'], }))); ``` For a bundled browser UI, use the browser entry. Its browser export excludes the standalone Node worker and Node resource cache: ```js import { createSticker, validateStickerJob } from '@open-wa/stickers/browser'; const job = { effects: ['comic'] }; validateStickerJob(job); const result = await createSticker(file, job); // Download or preview result.bytes. Cleanup is automatic. ``` Job validation does not read media or start rendering. The browser runtime uses the same job planner, renderer and output limits as the other execution hosts. `listEffects({ backend: 'browser', availableOnly: true })` includes option descriptors alongside defaults: numeric bounds, integer requirements and slider steps, string lengths, and point-coordinate bounds. The planner uses those same numeric bounds, so authoring controls can follow the public catalogue. Browser font verification needs a secure context (HTTPS or localhost). Animated input decoding also depends on the browser's codec support. A browser capability failure leaves server-side code export available; it does not create a successful local render. `createSticker(input, job)` defaults to `backend: 'auto'`. Before decoding, the planner compares the input codec and every ordered effect with the available browser capabilities. If all requirements fit, the job runs in that browser. Otherwise the complete job runs standalone. An unsupported complete job fails with `CAPABILITY_UNAVAILABLE`; it never transfers intermediate frames between backends or calls a rendering server. Explicit `browser` and `standalone` overrides are strict. Outside a browser, standalone is the default. For an existing page/driver: ```js import { createStickerRuntime } from '@open-wa/stickers'; import { createBrowserBackend } from '@open-wa/stickers/browser'; const stickers = createStickerRuntime({ browser: createBrowserBackend(page) }); const result = await stickers.createSticker(input, { effects: ['invert'] }); ``` The page must provide `evaluate(fn, argument)`. Inside a browser page the default runtime uses that page directly. Explicit runtimes also clean up automatically; their optional `dispose()` method is for an immediate, permanent shutdown. Bursts wait in arrival order before inputs are read, downloaded or copied, rather than failing when four requests are queued. Node prepares at most two jobs at once and lazily starts up to two standalone workers, leaving a CPU core available where possible. Browser jobs render one at a time and yield between CPU-heavy pixel batches, effects and frames so the page can process input and cancellation. Waiting jobs retain their original input references; callers still own the memory used by those inputs and any completed results they retain. Warm renderers are reused across the burst. After 60 seconds without work, owned workers, WASM instances, canvases and fonts are released; the next call recreates them automatically. Idle Node workers and cleanup timers do not keep the process alive. Cancelled or timed-out standalone jobs terminate their worker, and later jobs continue with a replacement. Each running job has a two-minute execution budget, including input loading; time spent waiting in the outer queue does not consume that budget. Browser cancellation is observed between pixel batches, effects and frames; an active native codec operation can finish before cleanup. The CLI releases its resources when the command ends. ## CLI [#cli] ```sh openwa-sticker photo.jpg --effects comic,tint,sparkle --output sticker.webp openwa-sticker photo.jpg --job sticker-job.json --output sticker.webp openwa-sticker effects openwa-sticker effects --all ``` `--job` supplies the same JSON job fields as the library. `--offline` requires previously cached resources; `--cache-dir` selects the resource cache. By default, resources live under `$XDG_CACHE_HOME/openwa/stickers/v1`, or `~/.cache/openwa/stickers/v1`. Resources are pinned by version, length and SHA-256. They download on demand and are verified before execution. Proxies and custom certificate authorities must be configured for Node's network client. ## Current coverage [#current-coverage] This first implementation contains 62 shared recipes: 57 versioned OpenWA-authored recipes and five additional numerical effects. Both backends use the same recipe definitions; Skia supplies standalone Canvas operations, and libwebp supplies standalone encoding. Invert, grayscale, stereo, dilation, erosion and Bayer expansion use shared Rust SIMD WASM. The other effects currently use JavaScript and their context's compositor. This is not a claim of historical Jeyy parity. PNG, JPEG, GIF and WebP are implemented in both contexts, including animated inputs. AVIF is available in browsers with an appropriate `ImageDecoder`. Standalone AVIF and MP4 are not yet implemented. The historical catalogue is included as an inventory; its unported entries fail explicitly. Use `listEffects({ availableOnly: true })` to enumerate executable recipes. Scene/simulation recipes, remaining historical ports, background removal and statistics reporting remain separate epic work. Custom colours use the portable sRGB subset: hex, named colours and comma-separated RGB/HSL. Jobs accept up to eight ordered effects. `freeze` keeps the first frame at its position in the stack; subsequent animated effects can introduce new motion. `fps` samples the selected timeline without speeding it up. `slow` changes frame durations. `trim` uses source milliseconds. `quality` is a maximum: the encoder can lower it to meet the byte budget and fails if the output still cannot fit. Text recipes lazily load pinned portable fonts in either context. ## Resource costs and limits [#resource-costs-and-limits] | Component | Extra download | Lifetime | | ---------------------------------------- | ---------------------------: | --------------------------------------- | | Public source package and shared kernels | About 247 KB unpacked | Installed once per package copy | | Shared Rust kernel binary | 66,478 bytes, included above | Instantiated in the selected renderer | | Standalone CanvasKit loader + WASM | 7,438,222 bytes | Disk cache; worker lives while hot | | Standalone libwebp loader + SIMD WASM | 384,230 bytes | Disk cache; worker lives while hot | | Portable fonts | 46,016–212,196 bytes each | Download only for selected text recipes | Browsers use their existing Canvas and codecs and download neither standalone compositor nor encoder. A 512×512 RGBA surface consumes 1 MiB; recipes can own several temporary surfaces during a frame. Frames are encoded sequentially. WASM heaps, Skia allocations and browser/worker overhead add memory beyond those surfaces. Peak RAM hasn't been measured for this implementation; the worker's 128 MiB JavaScript heap limit doesn't cap its native/WASM allocations. The admission profile is 512×512, at most 120 frames / 10 seconds, 20 MiB input, 16 million source pixels, and 100,000 / 512,000 output bytes for static / animated stickers. Rendering reserves 4,096 bytes for final metadata. The private patch checks final bytes again. An unknown send outcome is never retried automatically. WASM and client patches can be inspected and modified by a machine's owner. Donor protection applies to the supported OpenWA send path, not to arbitrary modified clients or externally generated files. # @open-wa/wa-automate-types-only (/docs/reference/workspaces/packages/types-only) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/types-only/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/types-only/README.md) # @open-wa/wa-automate-types-only [#open-wawa-automate-types-only] Lightweight type-only re-exports from @open-wa/schema — zero runtime dependencies Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/wa-automate-types-only ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/ui-components (/docs/reference/workspaces/packages/ui-components) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/ui-components/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/ui-components/README.md) # @open-wa/ui-components [#open-waui-components] Reusable React UI components for open-wa web applications. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/ui-components ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/utils (/docs/reference/workspaces/packages/utils) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/utils/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/utils/README.md) # @open-wa/utils [#open-wautils] Shared utility functions for open-wa Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash pnpm add @open-wa/utils ``` ## Documentation [#documentation] See the [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # @open-wa/wa-automate (/docs/reference/workspaces/packages/wa-automate) {/* Generated by `apps/docs/scripts/gen-workspace-readme-docs.js`; edit the workspace README and regenerate this page. */} > Source: [`packages/wa-automate/README.md`](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/wa-automate/README.md) # @open-wa/wa-automate [#open-wawa-automate] `@open-wa/wa-automate` runs an Open-WA session and exposes its client methods through an HTTP API. It also exports the session client and `createClient` for applications that need direct control in the same Node.js process. Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs). ## Install [#install] ```bash npm install @open-wa/wa-automate@5.1.0 ``` ## Start a local API server [#start-a-local-api-server] ```bash npx @open-wa/wa-automate@5.1.0 --session-id main --host 127.0.0.1 --port 8080 --api-key replace-this-key ``` Scan the QR code shown in the terminal to sign in. Check `http://127.0.0.1:8080/health` until it reports `connected: true` and `session.ready: true`. Keep the API on loopback or a private network unless you have configured an appropriate access boundary. Connect from another Node.js application with [`@open-wa/socket-client`](https://openwa.dev/docs/client-and-integrations/socket-client). ## Documentation [#documentation] See the [CLI reference](https://openwa.dev/docs/guides/configuration-and-cli), [custom code guide](https://openwa.dev/docs/getting-started/custom-code), and [docs site](https://openwa.dev). ## License [#license] [H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) — Hippocratic + Do Not Harm # v5 Release Status (/docs/releases/v5-alpha) The npm examples on this page pin **5.1.0**. The npm registry reported `latest=5.1.0` and `alpha=5.0.0-alpha.8` on 2026-09-29. Docker Hub listed only its moving `latest` tag, so the Docker image version could not be pinned to the npm release. Choose `alpha` only for deliberate prerelease work. This release page records the registry result observed on 2026-09-29. Generated API reference pages describe the package build that produced them; verify exact routes and return shapes against the version you deploy. ## Install the example version [#install-the-example-version] ```bash npx @open-wa/wa-automate@5.1.0 --session-id quickstart --host 127.0.0.1 --port 8080 ``` The Docker guide uses the only listed image tag, `latest`, which can move independently of npm `5.1.0`. The [getting-started pages](/docs/getting-started/quickstart) add persistent profiles, API keys, readiness checks, and first-message steps. `/api-docs/` proves that the HTTP route is live; require `/health` to report `connected: true` and `session.ready: true` before calling messaging methods. ## What changed in v5 [#what-changed-in-v5] * The project is a monorepo with separate runtime, API, schema, browser-driver, plugin, and integration packages. * Embedded applications create the runtime with `createClient` and use the exported `Client` facade for lifecycle and message methods. * Easy API commands use HTTP and expose generated API docs, while runtime events are available through the supported event surfaces. * Browser drivers are separate packages, so embedded applications can choose the driver that fits their environment. * Plugins load from `plugins` and receive their validated settings from `pluginConfig`. ## Alpha tags [#alpha-tags] The `alpha` dist-tag currently points to `5.0.0-alpha.8`. Select it only for deliberate prerelease work, record the exact tag in reports, and use a disposable account and session profile: ```bash npx @open-wa/wa-automate@alpha --session-id alpha-test --port 8080 ``` Prerelease selection does not change the supported release used by the main onboarding journey. ## v4 archive [#v4-archive] Keep existing v4 applications on their archived npm package while you migrate: ```bash npx @open-wa/wa-automate@4.76.0 ``` Docker Hub did not list a versioned v4 image tag when checked on 2026-09-29. Keep v4 configuration and session profiles separate from v5. New npm examples on this site target `5.1.0`. ## Safe migration sequence [#safe-migration-sequence] 1. Start a separate v5 session with Easy API and authenticate a disposable WhatsApp account. 2. Wait for `/health` to show both `connected: true` and `session.ready: true`. 3. Exercise the message, recovery, plugin, and webhook flows your application needs. 4. Use embedded `createClient` only when your application must own the browser and session lifecycle; use SocketClient for a remote Easy API consumer. This is unofficial software and is not affiliated with, authorized by, maintained by, sponsored by, or endorsed by WhatsApp or its affiliates. Automation can violate WhatsApp's terms of service, and account restrictions or bans are possible. # V5.0: A new foundation (/docs/releases/v5.0) Three years in the making, V5 is a major change to how you run and extend OpenWA. WhatsApp automation still sits at the centre, but you now have a clearer choice about which process owns the browser and session—and how the rest of your application talks to it. V5 became npm's `latest` release with this launch. Existing V4 applications need a deliberate migration; installing V5 over an unchanged V4 application isn't a drop-in upgrade. ## Start with the way you want to run it [#start-with-the-way-you-want-to-run-it] **Run WhatsApp as an API.** Easy API owns browser setup, authentication, the session and HTTP hosting. Start it as a process and call its documented methods from your service. This is the shortest route when you want an API without managing a browser lifecycle in your application. **Connect your application remotely.** SocketClient connects a Node.js app to a running Easy API instance. Commands travel over HTTP and live events over Server-Sent Events. Your bot, worker or dashboard can consume a session without opening its own browser. **Embed the runtime when you need control.** Use `createClient`, choose a browser driver, and own startup and shutdown inside your application. V4 applications using `create` need to move to the V5 contract. Start with the [Easy API guide](/docs/getting-started/easy-api), the [SocketClient guide](/docs/client-and-integrations/socket-client), or the [embedded runtime guide](/docs/getting-started/custom-code). ## Extend OpenWA around your workflow [#extend-openwa-around-your-workflow] The runtime, API, schema, browser drivers and integrations now live in separate packages. Reusable behaviour loads through `plugins` and `pluginConfig`, with dedicated paths for webhooks, Chatwoot, S3 media handling, Cloudflare session proxying and Node-RED. That separation makes ownership clearer: the session runs in one place, and the integrations around it can be configured for the application you're building. The [configuration guide](/docs/guides/configuration-and-cli) explains how to connect those pieces. ## Discover the API you actually run [#discover-the-api-you-actually-run] The method schema powers interactive API documentation and generated metadata. Easy API exposes Swagger and Postman descriptions alongside its live methods, so you can inspect the service's actual contract while building an integration. For agent integrations, Easy API can expose that schema as MCP tools at `/mcp`. Discovery and execution require the Easy API key. Enable MCP through `wa.config.*`; the V5 CLI does not parse `--mcp`. ## Moving from V4 [#moving-from-v4] 1. **Keep existing deployments pinned while migrating.** Pin `4.76.0` for an application that still uses V4, and run V5 separately until you've moved its configuration and workflows. 2. **Choose the right consumer.** Move remote consumers to SocketClient and embedded applications to `createClient`. Check the methods and schemas your application calls against `/api-docs/` on your running Easy API. 3. **Update webhook configuration and receivers.** Configure `@open-wa/integration-webhook` through `wa.config.*`. The V5 CLI accepts `--webhook` but doesn't register delivery. Receivers now read `payload` from `{ webhookId, sessionId, event, payload, timestamp }`, rather than V4-style `data`. The [webhook guide](/docs/guides/webhooks-for-business) covers the new delivery path. Move one workflow at a time, including its login, named session and recovery behaviour, before switching production traffic. ## Package changes [#package-changes] The package split supports these runtime and integration choices. Version alignment and dependency updates sit in the [full package changelogs](https://github.com/open-wa/wa-automate-nodejs/releases/tag/v5.0.0), alongside the detailed source comparison. Start with the ownership and migration changes above when deciding what your application needs to update. # V5.1: Buttons are back (/docs/releases/v5.1) If you gave up on WhatsApp buttons, OpenWA 5.1 gives you a reason to try them again. Send reply buttons, lists, forms, image carousels and booking cards through one `sendInteractive(to, content)` API, with the same JSON document in the SDK and HTTP API. **Available to Insiders and above.** Update OpenWA and restart your licensed session to load the new capabilities. Illustration of a WhatsApp message with reply buttons *Choose a reply in the conversation; route it by its stable ID. These are illustrations from our interactive message examples. Recipient appearance varies by WhatsApp version.* ## Get answers your code can use [#get-answers-your-code-can-use] `onInteractiveResponse` gives you the selected reply or list row ID. `onFormResponse` gives you answers keyed by your own question IDs, including arrays for multiple selections: ```ts client.onFormResponse(({ answers, originalMessageId }) => { // { plan: 'pro', extras: ['delivery', 'gift'] } console.log(answers); console.log(originalMessageId); // Match the submission to the form you sent. }); ``` You can use those answers directly in a booking, support or onboarding workflow. There's no need to scrape the displayed reply or work out which translated option a person selected. Keep your application IDs stable while changing the labels your customers see. Responses include the sender, chat, response message ID and original message reference, and HTTP integrations can subscribe to `interactive.response` on the event stream. Subscribe before sending, and deduplicate responses by message ID when your integration reconnects or syncs history. Illustration of a submitted form beside its structured onFormResponse answers *A form submission becomes structured answers your application can use.* ## Start with a few lines [#start-with-a-few-lines] A customer shouldn't need to guess which phrase your bot understands. Give each reply a label they can read and an ID your application can act on: ```ts client.onInteractiveResponse(response => { if (response.type === 'reply') console.log(response.id); }); await client.sendInteractive(to, { type: 'buttons', body: 'What can we help you with?', actions: [ { type: 'reply', id: 'order', label: 'Track my order' }, { type: 'reply', id: 'support', label: 'Talk to support' }, ], }); ``` These examples use a started, licensed `client` and the recipient's chat ID in `to`. Keep the IDs stable and change or translate the labels whenever you need to; your routing code keeps working. ## More ways to build a conversation [#more-ways-to-build-a-conversation] | What you're building | Message type | | ----------------------------------------------- | -------------------------- | | A short menu or confirmation | Reply buttons | | A website link, phone action or copyable code | URL, call and copy buttons | | A longer menu grouped by category | A sectioned list | | Preferences, choices or a sequence of questions | A form | | Visual options people can browse and choose | An image carousel | | Appointment information with a details view | A booking card | Illustration of a carousel with Starter and Pro image cards and reply actions *Let people browse image cards and choose an option without leaving the conversation.* Illustration of a booking card and its appointment details view *Give an appointment its own card and details view, using the recipient app’s native layout.* Explore [all eight illustrated examples in the interactive messages guide](/docs/guides/interactive-messages), including URL/call/copy actions, sectioned lists and single- or multiple-choice forms. ## One document across your integrations [#one-document-across-your-integrations] The SDK and Easy API accept the same JSON message document. Send it to `POST /api/messages/sendInteractive`, then consume `interactive.response` from the HTTP event stream. Save a template and reuse it from a bot, service or workflow tool. `defineInteractiveMessage(...)` validates reusable documents and keeps their specific TypeScript type, with defaults and errors that point to the field that needs attention. For integrations that already construct protocol messages, `sendRawMessage` accepts raw WhatsApp protobuf Message objects. Native actions also have an explicit extension format. ## Upgrade [#upgrade] ```sh npm install @open-wa/wa-automate@^5.1.0 ``` Read the [interactive messages guide](/docs/guides/interactive-messages) for all message types, response handlers, media and HTTP examples, and recipient support limits. This is the new V5 API; use it for new integrations rather than carrying over V4 button calls. Recipient apps control rendering, so what appears can vary between WhatsApp versions. Send reply buttons separately from URL, call and copy actions. Booking cards use WhatsApp's calendar icon and details layout; custom booking icons and header images aren't supported here. ## Also improved [#also-improved] * **Bun package imports:** local development can resolve TypeScript source, while published packages resolve their distributed files. This shared fix applies across 30 packages. * **Event subscriptions:** registering the browser bridge again preserves installed event bindings. * **License readiness:** health responses report the applied, server-confirmed license state, with improvements to host-account resolution and bootstrap recovery. ## Package alignment [#package-alignment] The main interactive change spans the client, schema, API, core and Easy API packages. Coordinated version bumps and dependency updates in 22 packages keep the release aligned; they aren't 22 additional features. The [full release and package changelogs](https://github.com/open-wa/wa-automate-nodejs/releases/tag/v5.1.0) contain the technical details. # V5.4: Your session, your key (/docs/releases/v5.4) Your compact session can travel with an encryption key you control. OpenWA 5.4 accepts a key from your config, an environment variable or a mounted Docker secret, while keeping the encrypted session file as two values: `auth` and `data`. ## 🔒️ Choose your key [#️-choose-your-key] ```ts import { create } from '@open-wa/wa-automate'; const client = await create({ sessionId: 'sales', sessionDataEncryptionKeyFile: '/run/secrets/openwa-session-key', }); ``` The file contains a base64-encoded 32-byte random key. Alternatively, pass `sessionDataEncryptionKey` or set `OPENWA_SESSION_DATA_ENCRYPTION_KEY`. The environment variable is read automatically. Use one key source, and keep the same key and session ID across containers. Store the key separately from the session file. Omit the key settings to use OpenWA's baked default. Existing filename, path and S3 settings remain available. ## 🚚 Pairing and resets [#-pairing-and-resets] Compact authentication is the only path; `legacyDataDirAuth` is removed. Moving from a browser profile or changing keys requires a fresh QR scan. Reset a saved session with `sessionData: 'NUKE'`, then remove that setting after pairing. Key settings are redacted from debug responses and startup events. Verbose environment logs show variable names only. Live browser/S3 acceptance for this integration remains unverified. Read the [session storage guide](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/wa-automate/PORTABLE_SESSIONS.md) for storage and pending-message replay details. ## 🔖 Version alignment [#-version-alignment] 22 companion packages move to 5.4.0 with no separate feature changes. # V5.5: Events, live refresh and local stickers (/docs/releases/v5.5) OpenWA 5.5 adds native event APIs, refresh controls for running sessions and a local sticker renderer. Upgrade with `npm install @open-wa/wa-automate@5.5.0`. ## ✨ Events and attendance [#-events-and-attendance] ```ts const eventId = await client.sendEvent(chatId, { name: 'Community meetup', startTime: Math.floor(Date.now() / 1000) + 3600, }); client.onEventResponse(response => { console.log(response.eventId, response.response); }); ``` Create, edit and cancel events, read paginated synced invitations and attendance, and respond with `going`, `notGoing` or `maybe`. Event and RSVP snapshots have Effect schemas and JSON serializers. Event times use Unix seconds; RSVP timestamps use milliseconds. See the [events guide](/docs/guides/whatsapp-events). ## 🔄 Refresh a running session [#-refresh-a-running-session] Use **Refresh patches** or **Apply license** in the dashboard, or call `client.refresh()`. The Node process, API host and client objects stay alive while WhatsApp reloads. Messages may be missed while the page is unavailable, and interrupted operations aren't replayed. Progress is available through the SDK and authenticated API. See [session refresh](/docs/operations/session-refresh). ## 🎨 Local stickers [#-local-stickers] Install `@open-wa/stickers@5.5.0` to render WebP files locally: ```sh npx --package @open-wa/stickers@5.5.0 openwa-sticker photo.jpg --output sticker.webp --effects comic,sparkle ``` The SDK lazily loads the package. It selects the existing browser if that browser can complete the whole job; otherwise it selects standalone execution for the whole job. Effects preserve their specified order, and compositor, encoder and font resources download on demand. This first slice contains 62 recipes; the remaining historical effects and background removal are separate work. [Package usage and resource limits](https://github.com/open-wa/wa-automate-nodejs/blob/master/packages/stickers/README.md). Local rendering ships independently of sending. `client.sendSticker` requires the private donor finalizer, whose deployment remains separate from this public package release. ## Media and message controls [#media-and-message-controls] Media captions and quoted IDs now belong in the `sendFile`/`sendImage` options object. Add view-once attachments, prepared circular video notes, voice notes, looping videos and document presentation through typed options. See the [media guide](/docs/guides/media). Poll voting, synced results and vote listeners are available alongside message pinning and unpinning. Advanced poll closing and voter-name options depend on WhatsApp flags. Voluntary phone-number requests and sharing have SDK methods, but share replies failed on the test devices. Scheduled-call cards exposed RSVP/calendar controls; a Join control wasn't confirmed. Patch-backed file sending, URL downloads, message edits and reactions are restored, along with group contacts and participant listeners. Calling audio remains a licensed preview. Chrome, Chromium and Lightpanda provision on demand; Lightpanda remains experimental, with recovery and full media support unconfirmed. # V5.6: Events you can build on (/docs/releases/v5.6) OpenWA 5.6 brings generated event contracts, signed MCP subscriptions, and an Effect-backed emitter that keeps familiar callbacks while adding optional queued consumers. It also fixes the Node import failure reported in [#3534](https://github.com/open-wa/wa-automate-nodejs/issues/3534). ## Familiar callbacks, optional Effect streams [#familiar-callbacks-optional-effect-streams] The shared `ev` and socket clients now use HyperEmitter instead of a direct EventEmitter2 dependency. Ordinary callbacks still run synchronously, with variadic arguments, namespace wildcards, listener context, and the existing subscription helpers. ```ts import { ev } from '@open-wa/wa-automate'; ev.on('app.job.finished', ({ jobId }) => console.log(jobId)); ev.emit('app.job.finished', { jobId: 'daily-summary' }); ``` Use `onScoped()` for listeners whose lifetime belongs to an Effect scope, `emitEffect()` to await direct listener work inside Effect, or `subscribe()` and `stream()` for independent queued consumers. `pubsub()` exposes the native Effect PubSub on the same emitter. Callback-only use creates no hub. Queued observation defaults to no replay and unbounded pending retention for active subscribers. Dropping and sliding policies accept an explicit capacity and can lose observations. `emitAsync()` awaits direct callbacks; queued consumers own their completion and retry policy. Closing a consumer scope releases its subscription, and closing an owned emitter interrupts its work without promising to drain its backlog. [Effect event guide](https://openwa.dev/docs/guides/effect-event-streams) · [Emitter reference](https://openwa.dev/docs/reference/hyperemitter) · [Session events](https://openwa.dev/docs/guides/session-events) ## Know the payload before building your receiver [#know-the-payload-before-building-your-receiver] Download the generated AsyncAPI 3.1 contract from Easy API's `/meta/asyncapi.json`, or browse the event reference. The schema-owned catalog documents six runtime events across SSE and outbound webhooks, plus the three compact MCP subscription payloads. Callback names and transport envelopes are described separately, so your receiver can use the wire shape for its delivery surface. [Event contract reference](https://openwa.dev/docs/reference/event-contracts) · [AsyncAPI guide](https://openwa.dev/blog/read-openwa-events-with-asyncapi) ## Give ChatGPT a WhatsApp conversation to watch [#give-chatgpt-a-whatsapp-conversation-to-watch] Easy API's authenticated MCP endpoint supports subscriptions to incoming messages, acknowledgements, and session state changes. Clients can filter a conversation where the snapshot contains its chat ID. OpenWA verifies callbacks, signs deliveries with Standard Webhooks, validates public HTTPS destinations, and rejects redirects. Subscriptions and pending deliveries persist in a private SQLite journal. They have finite expiration; pending requests retain their event IDs across bounded retries. Keep `.open-wa/` on persistent storage. Deliveries may repeat or arrive out of order, and events missed while OpenWA is offline aren't replayed. Message summaries contain up to 4,000 characters and identify truncation. Hosted MCP serves protocol `2026-07-28`; use a compatible MCP 2.0 client and a publicly reachable HTTPS endpoint. HTTP acknowledgement confirms receipt of a callback, rather than completion of the client's task. Live ChatGPT delivery isn't established by the package and contract checks in this release. [MCP Events setup](https://openwa.dev/docs/client-and-integrations/mcp-events) · [ChatGPT conversation guide](https://openwa.dev/blog/watch-whatsapp-from-chatgpt) ## Upgrade [#upgrade] ```sh npm install @open-wa/wa-automate@5.6.0 @open-wa/driver-puppeteer@5.6.0 # For remote Easy API clients: npm install @open-wa/socket-client@5.6.0 ``` All 28 packages in the fixed OpenWA group move to 5.6.0. `@open-wa/runtime-node` moves to 5.1.2 for its updated internal dependencies. [Full diff](https://github.com/open-wa/wa-automate-nodejs/compare/v5.5.1...v5.6.0)