WhatsApp Business CLI
- Agents and operations
- Operations CLI
- 2026
- Open source
Go, Cobra, Meta Graph API, WhatsApp Business Management API
WhatsApp Business CLI reads one WhatsApp Business Account through Meta’s Graph API. It covers phone numbers with their quality rating and messaging limit, message templates with their review status, and message and pricing analytics. It cannot send a message or change a template. Every request it makes is a GET, and that is the whole design.
I built it for agents run through Reckon, which needed answers about numbers, templates and spend for sales and support work. A general Graph client would also carry the send path. With a business token in an agent’s hands, I wanted safety to come from the binary, not from a prompt.
whatsapp commands resolve -- numbers list | jq -r .effectreadwhatsapp commands resolve -- messages sendError: arguments do not name a runnable whatsapp commandwhatsapp analytics conversationsError: unknown command "conversations" for "whatsapp analytics"whatsapp analytics pricing --from 2025-06-01Error: Meta keeps analytics for one year; move --from later
4 commands4 lines of output
- Chapter
- Agents and operations
- Category
- Operations CLI
- Year
- Availability
- Open source
- Last commit
- Go
- Cobra
- Meta Graph API
- WhatsApp Business Management API
Reads by construction
internal/graph/graph.go has one request method, Client.Get, which sends the token as a bearer header and never retries. No code path builds a POST, so read-only is not a mode to switch on. The test server in cmd/root_test.go fails any request whose method is not GET. Settings come only from the environment, with no config file, login or profile, so there are no local writes either. commands resolve reports read for every command, and fails for messages send because no such command exists.
What crosses the wire
Analytics are Graph field expressions, so flag values land inside a request string. One test expects pricing_analytics.start(1790812800).end(1791417600).granularity(DAILY).country_codes(["IN"]).dimensions(["PRICING_CATEGORY","COUNTRY"]). Every value is checked first. Dimensions come from a fixed list per metric, countries must be two letters, phone numbers normalise to digits, and cursors match a plain character set. The tests feed in COUNTRY]).metric_types([COST and x&access_token=y, and both are refused before a request. The token travels only in a header. Graph’s paging.next and paging.previous links can carry it, so output drops both and keeps paging.cursors for --after. Errors keep Meta’s code, subcode and trace id. Exit status 1 means a usage or local error, 3 a Graph API error, and 4 an unreachable API.
A spend question
export WHATSAPP_ACCESS_TOKEN=... # system user token
export WHATSAPP_BUSINESS_ACCOUNT_ID=1029...
whatsapp commands resolve -- analytics pricing --dimension tier
whatsapp templates list --status rejected --category utility
whatsapp analytics pricing --from 2026-10-01 --to 2026-10-08 \
--dimension pricing_category --dimension country
The resolver needs no credentials and uses the real parser, and Reckon consults it before running a command. Rejected templates come back with rejected_reason. Dates are UTC days with --to exclusive, and a window older than a year is refused, since Meta keeps a year of analytics.
What read-only costs
- Nothing gets fixed from here. A rejected template or a low quality rating is reported, and the change happens somewhere else.
- Template analytics are absent. Meta wants a one-way opt-in POST on the account before it serves them, and this tool cannot make that call.
- Conversation analytics are absent too. Conversation-based pricing ended on 2025-07-01, so the one-year window holds nothing. Messaging-limit history is not covered either.
- Environment-only settings mean one account per process, and JSON is the only output.
- The one I would revisit is
WHATSAPP_GRAPH_URL. It exists for tests, yet the shipped binary reads it, and whatever host it names receives the token. Reckon pins it empty, and the CLI should not need that.
Where it stands
The history is three commits, starting on 2026-10-14, and v0.1.0 was tagged on 2026-10-20. Field names follow Meta’s documentation as of October 2026 and have not yet been run against a live account. The default Graph version is v25.0, and anything older than v24.0 is refused before a request. There is no update notifier and no background request. CI tests it on Linux, macOS and Windows, and it is MIT licensed.