Wabery CLI
The Wabery CLI manages channels, registration intents, contacts, imports, broadcasts, messages, flows, hosted functions, automations, and webhooks from your terminal. It also ships the Wabery MCP server, so you can hand Wabery to an AI coding agent (Claude, Codex, OpenCode) and build by describing the feature.
Install & sign in
Section titled “Install & sign in”Install it globally, then sign in:
npm i -g @wabery/cliwabery loginpnpm add -g @wabery/cliwabery loginbun add -g @wabery/cliwabery loginwabery login authorizes the CLI in your browser (--no-open prints a URL
instead), asks you to choose a project, creates a key scoped to that project,
then shows the key once. Paste it back into the waiting terminal; the CLI stores
it locally so future CLI and MCP commands are authenticated. Run wabery doctor
to confirm. To override, set WABERY_API_KEY (and optionally WABERY_BASE_URL,
which defaults to the API base https://api.wabery.com/v1 — not the dashboard
origin).
Run without installing
Section titled “Run without installing”Prefer not to install globally? Run any wabery command on demand through your
package runner — handy for CI or a one-off wabery login:
npx wabery loginnpx wabery doctorpnpm dlx wabery loginpnpm dlx wabery doctorbunx wabery loginbunx wabery doctorScaffold app files
Section titled “Scaffold app files”After login, wabery init creates starter files for your app without
overwriting existing files:
wabery init --name "My project"It writes .env.wabery, wabery.config.json, and a
wabery-webhook-handler.ts stub. Load .env.wabery in your app, edit the
webhook URL in wabery.config.json, then apply it with
wabery config apply wabery.config.json. Config apply can create or update a
hosted-agent Draft only. It cannot edit Live or publish the agent to customers.
Common commands
Section titled “Common commands”# verify your API key and base URL; also flags channels that drop inbound# (the `channels` check lists any `routing_warnings`)wabery doctor
# confirm the project this key is scoped towabery projects list
# for a new sandbox project: text the join code to the sandbox,# send one normal WhatsApp message, then inspect the conversationwabery projects get "project_id" # read join_code + enrollment_channel.phone_numberwabery channels listwabery conversations listwabery conversations messages "conversation_id"
# reply inside the open messaging windowwabery messages send --channel-id "channel_id" \ --conversation-id "conversation_id" \ --text "Thanks for your message"
# send richer WhatsApp payloadswabery messages send --channel-id "channel_id" \ --conversation-id "conversation_id" \ --latitude 37.7749 --longitude -122.4194 --location-name "Office"wabery messages send --channel-id "channel_id" \ --conversation-id "conversation_id" \ --sticker-link "https://example.com/sticker.webp"
# create a starter config, then validate itwabery config init wabery.config.json --project-id "project_id"wabery config validate wabery.config.json
# enroll a WhatsApp contact for testing (optionally with a language)wabery contacts enroll --project-id "project_id" --phone "+14155550123" \ --preferred-language "es"
# update a contact's language or metadata later (metadata is shallow-merged)wabery contacts update "contact_id" --preferred-language "id" \ --metadata '{"linked": true}'
# import a consented CSV audience, then poll the returned import idwabery contacts import contacts.csv --phone-column phone \ --name-column name --language-column language --tags-column tags \ --consent '{"categories":["MARKETING"],"source":"signup_form","method":"web_form"}'wabery contacts import-status "import_id"
# mint a single-use WhatsApp registration linkwabery registration-intents create --customer-reference "user_123" \ --metadata '{"plan": "starter"}'wabery registration-intents get "intent_id"
# send a flow; use --config-key to pick the variant for the contact's languagewabery flows send "flow_id" --channel-id "channel_id" --contact-id "contact_id"wabery flows send --config-key "lead_intake" --locale "es" \ --channel-id "channel_id" --contact-id "contact_id"
# check a queued message or wait for a template approval in scriptswabery messages get "message_id"wabery templates wait "template_id" --wait=86400
# inspect channels; `routing_warning` flags a channel that silently drops inboundwabery channels listwabery channels get "channel_id"
# forward a dedicated number's inbound to your webhook (overrides project routing)wabery channels update "channel_id" --routing-mode EXTERNAL
# or set the project-wide default that new channels inheritwabery projects update "project_id" --routing-mode EXTERNAL
# deploy hosted logic for tools or message-triggered workflowswabery functions create --slug quote-customer \ --name "Quote customer" --trigger-type ANY_MESSAGEwabery functions deploy "function_id" --source ./quote-customer.tswabery functions update "function_id" --active truewabery functions test "function_id" --text "I need a quote"wabery functions update "function_id" \ --expose-as-mcp-tool true \ --input-schema '{"type":"object"}'wabery functions invoke "function_id" --arguments '{"orderId":"ord_123"}'wabery functions logs "function_id"Broadcast workflow
Section titled “Broadcast workflow”A broadcast sends an approved WhatsApp template to a frozen, consent-eligible audience. It requires a dedicated Meta Cloud API channel; the shared sandbox cannot send broadcasts. Prepare and inspect the audience before the irreversible send step.
First create broadcast.json:
{ "name": "August launch", "channelId": "channel_...", "category": "MARKETING", "templateName": "august_launch", "fallbackLanguage": "en_US", "variants": [ { "templateId": "template_...", "language": "en_US", "parameterMappings": [ { "component": "body", "index": 0, "parameterType": "text", "source": "name", "fallback": "there" } ] } ]}Then create, prepare, and review the draft:
wabery broadcasts create --body @broadcast.json
# audience.json must contain exactly one of contactIds, importId, or filterwabery broadcasts prepare "broadcast_id" \ --body '{"filter":{"tags":["launch-list"],"languages":["en","es"]}}'
# preparation is asynchronous: poll until status is readywabery broadcasts get "broadcast_id"
# inspect the frozen snapshot, including skipped or failed recipientswabery broadcasts recipients "broadcast_id"wabery broadcasts recipients "broadcast_id" --limit 100wabery broadcasts recipients "broadcast_id" --status skippedOnly after the returned counts match the intended audience, send now or schedule an absolute instant:
wabery broadcasts send "broadcast_id" \ --idempotency-key "august-launch-send"
wabery broadcasts schedule "broadcast_id" \ --scheduled-at "2026-08-01T10:00:00Z" \ --timezone "Europe/Amsterdam" \ --idempotency-key "august-launch-schedule"Monitor, reuse, or stop a campaign:
wabery broadcasts list --status sendingwabery broadcasts get "broadcast_id"wabery broadcasts duplicate "broadcast_id"wabery broadcasts cancel "broadcast_id"The CLI generates an idempotency key for mutating broadcast commands when one is not supplied. Pass your own stable key in scripts. Duplicating copies the template configuration into a new draft, not the previous audience snapshot. Canceling stops recipients that have not been dispatched; messages already accepted by Meta cannot be recalled. See WhatsApp broadcasts for consent, frequency, status, and webhook behavior.
MCP server
Section titled “MCP server”The CLI also prints config for the local Wabery MCP server, so Claude Code, Codex, and OpenCode can call Wabery directly:
wabery mcp-config claude --rawwabery mcp-config codex --rawwabery mcp-config opencode --rawPrefer no install? Wabery also has a hosted MCP server with OAuth sign-in. See the WhatsApp MCP Server docs for the hosted setup, the full list of exposed tools, and a worked example prompt.