@wabery/cli
v0.11.5
Published
Wabery CLI and MCP server for building WhatsApp, Instagram, and Messenger automations with Wabery.
Maintainers
Readme
Wabery CLI and MCP Server
Wabery is a developer platform for building messaging workflows on WhatsApp, Instagram, and Messenger. It provides APIs for channels, contacts, conversations, messages, WhatsApp Flows, hosted functions, automations, dispatches, submissions, registration intents, and signed webhooks, so you can build your own product experience instead of wiring together a closed inbox tool.
This package ships the wabery command and a local MCP server for AI coding
agents such as Claude Code, Codex, and OpenCode.
Full documentation: https://www.wabery.com/docs
Setup
Install the CLI, then sign in through your browser:
npm i -g @wabery/cli
wabery loginNo global install is required when the wabery runner package is available:
npx wabery login
npx wabery doctorwabery login prints an authorization URL and tries to open it in your
browser. Approve the request on app.wabery.com, choose the project the CLI
should manage, copy the one-time API key the page shows you, and paste it back
into the terminal. The key is scoped to that project and stored in your local CLI
config file. No local callback server is required — if the browser does not
open, just open the printed URL yourself. Use wabery login --no-open to skip
the open attempt. You can still override credentials explicitly:
export WABERY_API_KEY="wab_live_..."
export WABERY_BASE_URL="https://api.wabery.com/v1"Do not pass real keys to wabery mcp-config --api-key; generated MCP snippets
use a placeholder so credentials can live in your CLI config, shell, MCP client
secret storage, or another local secret manager. Public API keys support scoped
permissions. Use the smallest scope set the integration needs.
Working across multiple projects
API keys are scoped to a single project, so the CLI stores one key per project and auto-selects the right one based on the repository you run from.
wabery login(run once per project) stores that project's key in the global config and, the first time, writes a committed, secret-free.wabery.json({"project_id": "..."}) at the repo root.- When you
cdinto a repo, the CLI walks up from the current directory to find.wabery.json(falling back to awabery.config.json'sproject_id) and uses the matching stored key automatically — no flags needed. wabery use <project-id-or-name>binds the current repo to a different project you've logged into (rewrites.wabery.json).wabery whoamishows the active key (masked), org, project, and how it was resolved.wabery logout [--project <id|name>]removes one stored key;wabery logout --allclears them all.
.wabery.json is safe to commit — it never contains a key. A teammate who
clones the repo runs wabery login once to add their own key for that project.
Resolution precedence for which key is used: --api-key / WABERY_API_KEY >
the stored key for the project from --project-id > WABERY_PROJECT_ID >
.wabery.json > (if exactly one project is stored) that one.
Print a ready-to-paste env block for a project (API key, base URL, project id, channel id, webhook URL, and the webhook signing secret):
wabery env --project-id "proj_id"The signing secret is read from GET /projects/{id}, which reveals it for a
single project (the list never does). If a project has no secret yet, set one
from your own secret manager (bring-your-own) so both sides stay in sync:
# Set name / webhook URL / a known signing secret on an existing project
wabery projects update "proj_id" \
--webhook-url "https://yourapp.com/webhooks/wabery" \
--webhook-secret "$WABERY_WEBHOOK_SECRET"
# Or rotate to a fresh server-generated secret (returned once)
wabery projects rotate-secret "proj_id"
# Reveal a project (includes webhook_secret)
wabery projects get "proj_id"The same secret signs both inbound webhooks and outbound WhatsApp Flow data-exchange calls.
First sandbox message
For a brand-new project, use the shared Wabery sandbox before connecting your own WhatsApp Business number:
- Run
wabery login, choose the project, paste the one-time key back into the terminal, then runwabery doctor. - In the dashboard, send the project's join code (
wab-ABC123style) to the Wabery sandbox number from your test phone. From the CLI, runwabery projects get "project_id"to readjoin_codeandenrollment_channel.phone_number. - Send one normal WhatsApp message to the same sandbox chat. The join-code message only connects the phone; the normal message creates the visible conversation.
- Inspect and reply:
wabery channels list
wabery projects get "project_id"
wabery conversations list
wabery conversations messages "conversation_id"
wabery messages send \
--channel-id "channel_id" \
--conversation-id "conversation_id" \
--text "Thanks for your message"To scaffold app files after login, run wabery init --name "My project". It
writes .env.wabery, wabery.config.json, and wabery-webhook-handler.ts
without overwriting existing files unless you pass --force.
Channel routing
Inbound on a dedicated channel is handled by its own routing_mode, which
overrides the project's. wabery channels list shows a routing_warning when a
channel would silently drop inbound (e.g. a FLOWS channel under an EXTERNAL,
webhook-backed project). Point a number at your webhook:
wabery channels list
wabery channels get "channel_id"
wabery channels update "channel_id" --routing-mode EXTERNALYou can also set the project-wide default (inherited by new channels) with
wabery projects update "proj_id" --routing-mode EXTERNAL.
For local repository development, build the workspace package:
pnpm --filter @wabery/cli buildRun diagnostics and MCP config from the installed CLI:
wabery login
wabery doctor
wabery mcp-config claude --raw
wabery mcp-config codex --raw
wabery mcp-config opencode --raw
wabery mcpMCP starts in read-only mode. To allow mutation tools for a trusted local
session, set WABERY_MCP_MODE=write or generate config with --write.
High-risk tools also require confirmation tokens:
wabery_apply_config:confirmation_token: "apply_config"wabery_publish_flow:confirmation_token: "publish:<flow_id>"wabery_unenroll_contactwitherase=true:confirmation_token: "erase:<contact_id>"
Claude Code
Add a Wabery MCP server to .mcp.json:
wabery mcp-config claude --rawInstalled CLI:
{
"mcpServers": {
"wabery": {
"command": "wabery",
"args": ["mcp"],
"env": {
"WABERY_API_KEY": "wab_live_...",
"WABERY_BASE_URL": "https://api.wabery.com/v1"
}
}
}
}Codex
Add a Wabery MCP server to Codex config.toml:
wabery mcp-config codex --rawInstalled CLI:
[mcp_servers.wabery]
command = "wabery"
args = ["mcp"]
env = { WABERY_API_KEY = "wab_live_...", WABERY_BASE_URL = "https://api.wabery.com/v1" }OpenCode
Add a Wabery MCP server to OpenCode opencode.json:
wabery mcp-config opencode --rawInstalled CLI:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"wabery": {
"type": "local",
"command": ["wabery", "mcp"],
"enabled": true,
"environment": {
"WABERY_API_KEY": "wab_live_...",
"WABERY_BASE_URL": "https://api.wabery.com/v1"
}
}
}
}MCP Tools
wabery_check_connectionwabery_get_config_examplewabery_get_config_schemawabery_export_configwabery_validate_configwabery_preview_config_diffwabery_apply_configwabery_list_business_agentswabery_check_business_agent_eligibilitywabery_apply_business_agentwabery_get_business_agent_settingswabery_update_business_agent_settingswabery_list_business_agent_connectorswabery_create_business_agent_connectorwabery_create_business_agent_connector_toolwabery_list_business_agent_connector_toolswabery_get_business_agent_connector_toolwabery_update_business_agent_connector_toolwabery_delete_business_agent_connector_toolwabery_business_agent_thread_controlwabery_test_business_agentwabery_get_business_agent_evalwabery_list_business_agent_skillswabery_set_business_agent_skillswabery_list_projectswabery_get_projectwabery_update_projectwabery_rotate_webhook_secretwabery_list_channelswabery_get_channelwabery_update_channelwabery_create_registration_intentwabery_get_registration_intentwabery_get_limitswabery_list_flowswabery_get_flowwabery_publish_flowwabery_list_functionswabery_get_functionwabery_create_functionwabery_deploy_functionwabery_test_functionwabery_update_functionwabery_invoke_functionwabery_tail_function_logswabery_delete_functionwabery_enroll_contactwabery_list_contactswabery_get_contactwabery_unenroll_contactwabery_list_conversationswabery_get_conversationwabery_list_conversation_messageswabery_send_messagewabery_get_messagewabery_list_templateswabery_get_templatewabery_create_templatewabery_wait_templatewabery_send_flowwabery_list_submissionswabery_list_dispatcheswabery_get_dispatch
For agent workflows, inspect channel publish_readiness before template
creation. Templates require a dedicated non-sandbox WhatsApp channel, Meta
business verification, and a Meta payment method. Use preferred_language when
enrolling contacts if later flow sends should resolve locale-specific config
keys. After wabery_send_message, use wabery_get_message to inspect provider
status and Meta failure details. Use wabery_get_template with refresh or
wabery_wait_template to track approval.
Hosted function MCP tools let agents create project-scoped TypeScript handlers,
deploy source, test them, expose them as tool-callable logic, invoke them with
structured arguments, and read invocation logs. Write-mode function tools require
confirmation tokens such as create_function, deploy_function,
update_function, invoke_function, and delete_function.
Typical Developer Workflow
- List the project your key is scoped to:
wabery login
wabery doctor
wabery projects list- Enroll a WhatsApp phone number and capture the returned
contact_idandchannel_id:
wabery contacts enroll \
--project-id "proj_or_agent_id" \
--phone "+15555550123" \
--name "Jane Developer" \
--preferred-language "es" \
--reference-id "user_123"
wabery contacts get "contact_id"
# Update language / metadata later (metadata is shallow-merged):
wabery contacts update "contact_id" \
--preferred-language "id" \
--metadata '{"linked": true}'For self-serve customer onboarding, mint a single-use WhatsApp registration link and poll it with the publishable key used by your widget:
wabery registration-intents create \
--project-id "proj_or_agent_id" \
--customer-reference "user_123" \
--metadata '{"plan": "starter"}'
wabery registration-intents get "intent_id"- Generate and validate
wabery.config.json:
wabery config init wabery.config.json --project-id "proj_or_agent_id"
wabery config validate wabery.config.json
wabery config diff wabery.config.json
wabery config apply wabery.config.jsonwabery.config.json can manage the project's webhook declaratively, so
config apply keeps the webhook URL and signing secret in sync alongside flows:
{
"project_id": "proj_or_agent_id",
"webhook": {
"url": "https://yourapp.com/webhooks/wabery",
"signing_enabled": true,
"secret": "whsec_your_own_secret_from_your_secret_manager"
},
"flows": []
}Omit secret to have the server generate one; set signing_enabled: false to
clear it. The webhook setup is scriptable after the project exists: create or
select the project in the dashboard, create the project-scoped API key with
wabery login, run config apply to point the webhook, and run wabery env to
emit the secret for your deploy.
- Publish a flow explicitly after review. Without
--wait, this queues the publish job and returns immediately;--waitpolls until Meta reports it published, or fails with the rejection reason:
wabery flows list
wabery flows publish "flow_id" --wait # default 300s; --wait=600 for longer
wabery flows status "flow_id"--wait exits non-error while Meta is still publishing (publishing is async), and
prints Meta's real reason + validation_errors on a terminal failure instead of a
bare "Invalid parameter".
Inspect, debug, and clean up flows:
wabery flows get "flow_id" # includes the compiled flow_json + field_schema
wabery flows events "flow_id" # lifecycle log: publish/submission/reconcile detail
wabery flows reconcile "flow_id" # re-sync status from Meta (re-adopts a drifted flow)
wabery flows delete "flow_id" # draft → delete on Meta, published → deprecate- Send a published flow:
wabery channels list
wabery flows send "flow_id" --channel-id "channel_id" --contact-id "contact_id"For locale-aware sends, publish one flow per language sharing a config_key
(omit locale for the default) and send by key — Wabery resolves the variant for
the contact's preferred_language, falling back to the default:
wabery flows send --config-key "lead_intake" --locale "es" \
--channel-id "channel_id" --contact-id "contact_id"For sandbox projects, wabery contacts enroll returns the shared sandbox channel_id that is authorized for that phone number. Use that channel_id when sending a flow to the returned contact_id.
To remove a phone's sandbox authorization:
wabery contacts unenroll "contact_id"wabery config apply intentionally does not publish flows to Meta. Agents should call wabery_publish_flow only after explicit user confirmation and with the required confirmation token.
Template review can take longer than flow publishing. Templates require a dedicated WhatsApp channel with a Meta-verified business account and a Meta payment method; the Wabery sandbox cannot submit your templates. Use wabery channels get <channel_id> to inspect publish_readiness, then wabery templates status <template_id> to force a live Meta status refresh. For scripts, wabery templates wait <template_id> [--wait=86400] blocks until approval or fails on rejection/timeout. You can also subscribe to the project's signed template.status webhook event.
After sending a message, inspect the latest provider status with:
wabery messages get "message_id"Send richer WhatsApp payloads with first-class flags or --body:
wabery 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"- Deploy hosted logic for a workflow or Business Agent tool:
wabery functions create --slug quote-customer \
--name "Quote customer" --trigger-type ANY_MESSAGE
wabery functions deploy "function_id" --source ./quote-customer.ts
wabery 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"Use backing_type: "HOSTED_FUNCTION" and backing_function_id when a Meta
Business Agent connector tool should call hosted logic instead of a Flow or
external endpoint.
Local testing (no deploy, no API key)
Exercise your own webhook and data-exchange endpoints by signing a fake payload with your webhook secret and POSTing it to a local URL:
# Drive your webhook handler:
wabery webhooks send-test --url http://localhost:3000/webhooks \
--secret "$WABERY_WEBHOOK_SECRET" --event flow.completed
# Drive a message.received webhook with an inbound media file payload:
wabery webhooks send-test --url http://localhost:3000/webhooks \
--secret "$WABERY_WEBHOOK_SECRET" \
--payload '{
"event":"message.received",
"payload":{
"message_id":"msg_test",
"from":"+15551234567",
"text":null,
"assets":[{
"id":"asset_test",
"message_id":"msg_test",
"type":"image",
"url":"https://storage.wabery.com/assets/inbound/asset_test?token=...",
"expires_at":"2026-06-27T14:21:10.000Z",
"mime_type":"image/jpeg",
"file_name":"msg_test-image.jpg",
"file_size":248913,
"provider":"whatsapp",
"provider_media_id":"wamid...",
"status":"available"
}]
},
"sentAt":"2026-06-20T14:21:10.000Z"
}'
# Drive your data-exchange endpoint (one screen advance):
wabery flows test --url http://localhost:3000/flow \
--secret "$WABERY_WEBHOOK_SECRET" \
--action data_exchange --screen SELECT --data '{"workspace":"ws_1"}'
# Walk the whole journey, then fire flow.completed at your webhook:
wabery flows simulate --url http://localhost:3000/flow \
--webhook-url http://localhost:3000/webhooks \
--secret "$WABERY_WEBHOOK_SECRET" \
--steps '[{"action":"INIT"},{"action":"data_exchange","screen":"SELECT","data":{"workspace":"ws_1"}}]' \
--submission '{"workspace":"ws_1","project":"proj_a"}'Both send a valid x-wabery-signature so you can verify it exactly as in production.
The SDK's signPayload(body, secret) does the same for code-level fixtures.
Programmatic use
This package exposes two subpath entry points in addition to the wabery binary,
both fully typed:
// A thin authenticated HTTP client for the Wabery REST API.
import { WaberyApiClient, WaberyApiError } from "@wabery/cli/api-client";
const client = new WaberyApiClient({ apiKey: process.env.WABERY_API_KEY });
const projects = await client.get("/projects");
// The MCP server, to embed in your own host.
import { createWaberyMcpServer, runMcpServer } from "@wabery/cli/mcp-server";For building application features (sending messages, flows, webhooks, typed event
parsing), prefer the @wabery/sdk
package — it shares the same REST surface with richer types and helpers
(constructEvent, verifyDataExchange, the flow builders). @wabery/cli is
focused on local config management and MCP tooling for AI agents.
