wuapi

Link your own WhatsApp numbers by QR or pairing code and send and receive messages over a REST API. TypeScript and Rust SDKs, CLI, webhooks and an MCP server.

Hosted MCP Server

npx add-mcp 'https://wuapi.dev/api/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

wuapi MCP server

npm version CI license

A Model Context Protocol server for wuapi, the WhatsApp API for developers. It lets Claude, Cursor, VS Code and other MCP clients use your wuapi account: send messages, link numbers, read conversations, manage groups, communities, channels, labels, profile and privacy, webhooks, projects and invitations.

Every tool is a call to the wuapi REST API with your own API key, through @wuapidev/sdk. The API decides what the key may do, rate limits it (600 requests per minute per key) and logs each call in your dashboard, exactly as for any other client.

Docs: wuapi.dev/docs/mcp.

How it works. wuapi links your own numbers as devices, the same way WhatsApp Web works. It does not use the WhatsApp Business Platform. WhatsApp can restrict numbers that behave like spam: send only to people who expect your messages.

Two ways to connect

Local (stdio)Hosted (Streamable HTTP)
Runson your machine: npx -y @wuapidev/mcpat https://wuapi.dev/api/mcp
Keyyour npx @wuapidev/cli login, or the WUAPI_API_KEY environment variableAuthorization: Bearer wu_live_... header
NeedsNode 20 or latera client that sends custom headers

The quickest local setup uses the wuapi CLI: it logs you in from the browser, stores a key on your machine (~/.config/wuapi/credentials.json, readable only by you) and registers the server with your client, with no key in the client's config:

npx @wuapidev/cli login
npx @wuapidev/cli mcp add          # Claude Code, Cursor or VS Code

The local server uses WUAPI_API_KEY when the client sets it, and otherwise that stored login: the current profile, or the one WUAPI_PROFILE names (npx @wuapidev/cli profiles lists them). Or create an API key yourself at wuapi.dev/app/api-keys and pass it as below. A project key limits the server to one project.

Claude Code

# Local, with the key from `npx @wuapidev/cli login`
claude mcp add wuapi -- npx -y @wuapidev/mcp

# Local, with a key in the config
claude mcp add wuapi --env WUAPI_API_KEY=wu_live_... -- npx -y @wuapidev/mcp

# Hosted
claude mcp add --transport http wuapi https://wuapi.dev/api/mcp --header "Authorization: Bearer $WUAPI_API_KEY"

Claude Desktop

Settings > Developer > Edit Config, then add to claude_desktop_config.json:

{
  "mcpServers": {
    "wuapi": {
      "command": "npx",
      "args": ["-y", "@wuapidev/mcp"],
      "env": { "WUAPI_API_KEY": "wu_live_..." }
    }
  }
}

Cursor

~/.cursor/mcp.json (or .cursor/mcp.json in a project):

{
  "mcpServers": {
    "wuapi": {
      "command": "npx",
      "args": ["-y", "@wuapidev/mcp"],
      "env": { "WUAPI_API_KEY": "wu_live_..." }
    }
  }
}

Or the hosted server:

{
  "mcpServers": {
    "wuapi": {
      "url": "https://wuapi.dev/api/mcp",
      "headers": { "Authorization": "Bearer wu_live_..." }
    }
  }
}

VS Code

.vscode/mcp.json. VS Code asks for the key once and stores it outside the file:

{
  "inputs": [{ "type": "promptString", "id": "wuapi-key", "description": "wuapi API key", "password": true }],
  "servers": {
    "wuapi": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@wuapidev/mcp"],
      "env": { "WUAPI_API_KEY": "${input:wuapi-key}" }
    }
  }
}

For the hosted server use "type": "http", "url": "https://wuapi.dev/api/mcp" and "headers": { "Authorization": "Bearer ${input:wuapi-key}" }.

Other clients

Any client that starts a local command works with npx -y @wuapidev/mcp, with WUAPI_API_KEY in its environment or after npx @wuapidev/cli login. Any client that connects to a remote server with a custom header works with the hosted endpoint. Clients that only connect to remote servers through OAuth, as some chat apps do, cannot use the hosted endpoint yet.

Configuration

Variable
WUAPI_API_KEYYour API key, wu_live_.... When unset, the key npx @wuapidev/cli login stored is used.
WUAPI_PROFILEWhich stored login to use (npx @wuapidev/cli profiles). Default: the current one. Its project and base URL apply unless the two variables below are set.
WUAPI_PROJECTAct inside one project: its id or ext:<externalId>. Sent as Wuapi-Project.
WUAPI_BASE_URLAPI base URL. Default https://api.wuapi.dev. Must be https (or http on localhost).
WUAPI_MCP_READ_ONLYtrue registers only the tools that read. Same as the --read-only flag.

The hosted endpoint takes the same options as headers: Wuapi-Project: <id or ext:externalId> and Wuapi-Read-Only: true.

Tools

86 tools reach every operation of the REST API, except the two that return a secret and the two steps of an upload through an upload URL (see below). Tools that read are marked readOnlyHint. Tools that delete, revoke, reset, block or leave are marked destructiveHint and take confirm: true, which the model has to set on purpose and your client shows you before the call.

AreaTools
Contextget_current_key
Accountslist_accounts, get_account, get_account_qr_code, create_account, request_pairing_code, reconnect_account, list_proxy_locations, update_account, unlink_account, set_presence, set_disappearing_timer, reject_call
Messagessend_text, send_media, upload_file, send_location, send_contact, send_poll, reply_to_message, react_to_message, get_message, list_messages, edit_message, delete_message, cancel_message, vote_in_poll, forward_message, star_message
Chatslist_chats, get_chat, mark_chat_read, send_read_receipts, archive_chat, pin_chat, mute_chat, delete_chat, manage_labels
Contactslist_contacts, get_contact, check_numbers, lookup_contacts, save_contacts, lookup_whatsapp_info, manage_block_list
Profilemanage_profile, manage_favorite_stickers, manage_privacy
Groupslist_groups, get_group, create_group, add_group_participants, remove_group_participants, promote_group_participants, demote_group_participants, get_group_invite_link, reset_group_invite_link, leave_group, manage_group_settings, manage_group_joins, manage_community
Channelsmanage_channel
Storiespost_story, manage_stories
Webhookslist_webhooks, get_webhook, create_webhook, update_webhook, delete_webhook
Incoming webhookslist_incoming_webhooks, get_incoming_webhook, create_incoming_webhook, update_incoming_webhook, delete_incoming_webhook
Projectslist_projects, get_project, create_project, manage_project
Invitationscreate_invitation, list_invitations, get_invitation, cancel_invitation, resend_invitation, manage_branding
Usageget_usage, get_usage_by_project

Resources with many small operations are one tool with an action argument, so the list stays short enough for a model to pick from. The tool's description lists its actions, and each field says which actions use it. Only the destructive actions (marked below) need confirm: true.

ToolActions
unlink_accountlogout*, delete*
set_presenceonline, offline, typing, recording, paused, subscribe
manage_labelsupsert, delete*, label_chat, unlabel_chat, label_message, unlabel_message
save_contactsone, many, backfill_auto_save
lookup_whatsapp_infocontact_picture, business_profile, resolve_link, bots, sticker_pack, order
manage_block_listlist, block*, unblock
manage_profileupdate, set_picture, delete_picture*, get_contact_link, reset_contact_link*
manage_favorite_stickerslist, get_file, add, remove
manage_privacyget, get_story_privacy, update
manage_group_settingsupdate, set_picture, delete_picture*
manage_group_joinspreview_invite, join, list_requests, approve_requests, reject_requests
manage_communitycreate, list_groups, list_members, link_group, unlink_group
manage_channellist, get, preview_invite, list_messages, create, follow, unfollow, mute, unmute, react, mark_viewed
manage_storieslist, list_own, get, viewers, view, react, reply, delete*
manage_projectupdate, delete*, list_keys, revoke_key*
manage_brandingget, update

* needs confirm: true.

To post to a channel, use send_text or send_media with to set to the channel id. In read-only mode a tool that mixes reads and writes keeps only its reading actions (manage_channel keeps list, get, preview_invite and list_messages).

get_account_qr_code returns the QR code as an image your client can show. Projects, invitations, branding and usage need an organization key.

Not exposed: creating a project API key, rotating a webhook endpoint's signing secret and rotating an incoming webhook's URL. Both return a secret exactly once, and tool results never carry secrets, so a tool would create a key or secret nobody could read (and a rotation would break your server's signature check at once). Do both in the dashboard or with the SDK. Also not exposed: completing and reading an upload. They belong to the upload-URL flow, where the file's raw bytes are posted to a URL, which a tool cannot do; upload_file takes the bytes in its one call and returns the upload ready to send. Sending a test webhook event and reading request logs have no public API endpoint; they are in the dashboard.

src/operations.ts maps every API operation to the tools that call it; a test keeps it equal to the OpenAPI spec.

Resources and prompts

Resources: https://wuapi.dev/openapi.json (the OpenAPI 3.1 spec), https://wuapi.dev/llms-full.txt (the docs as Markdown), https://wuapi.dev/llms.txt (their index) and wuapi://webhook-events (every event type).

Prompts: send_message (to, message), setup_webhook (url, events), invite_customer (customer, externalId, email) and use_streams (project, language). use_streams helps you receive events with Streams, the live alternative to Webhooks for code with no public endpoint: with the SDK's events.stream in TypeScript and Rust, and the rules of a client in any other language.

Security

  • The key stays where you put it: in the client's configuration, a header, or the CLI's credentials file (mode 0600), which the server only reads. The server never logs it, never returns it, and never includes it in an error.
  • Tool results never contain secrets. A webhook endpoint's signing secret is returned by the API only once, when the endpoint is created; create_webhook drops it and tells you to reveal it in the dashboard instead. An incoming webhook's URL is a credential too: its tools drop it, and the user copies it from the dashboard. There is no tool that creates API keys.
  • Scope the key: a project key reaches one project, and WUAPI_MCP_READ_ONLY=true removes every tool that writes.
  • Media is fetched by the wuapi API, never by this server. The API refuses private and internal addresses.
  • Every argument is validated against the tool's JSON schema before any request. A base URL that is not https is refused, so the key never travels in clear text.

Use it as a library

import { Wuapi } from "@wuapidev/sdk";
import { createWuapiMcpServer } from "@wuapidev/mcp";
import { createWuapiMcpHttpHandler } from "@wuapidev/mcp/http";

// One server per client connection, acting as this key.
const server = createWuapiMcpServer({ client: new Wuapi({ apiKey: process.env.WUAPI_API_KEY }) });

// Or a stateless Streamable HTTP handler: (Request) => Promise<Response>,
// authenticated with each caller's own key as a bearer token.
const handle = createWuapiMcpHttpHandler();

License

MIT