CLI
npx @quami/sendo doctor, accounts add, list, set-default, disconnect and export, and rules sync: checks, flags and environment.
The package ships a sendo binary. Run it from your app’s root with npx @quami/sendo <command> (or npx sendo once installed). Output, prompts and errors are in English; --help lists every flag.
sendo doctor [--account <id>] [--url <url>] [--json] [--config <path>]
sendo accounts add [--phone-number-id <id>] [--waba-id <id>] [--token-env <VAR>]
[--tenant-id <uuid>] [--display-phone <number>] [--timezone <IANA>] [--yes]
sendo accounts list [--tenant-id <uuid>] [--json]
sendo accounts set-default <id>
sendo accounts disconnect <id>
sendo accounts export <id> [--out <file>]
sendo rules sync --config <path> [--force] [--tenant <id>]
sendo mcp [--config <path>]
sendo --help | --versionEnvironment#
Both commands load .env.development.local, .env.local and .env from the current directory, in that order, without overriding variables already set in the shell. To check production values, export them or run the command where they are defined.
| Variable | Used for |
|---|---|
SENDO_META_APP_SECRET | Required (env check), unless the Connect variables are set. |
SENDO_VERIFY_TOKEN | Required; also used for the --url challenge. |
SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL | Supabase project URL. |
SUPABASE_SERVICE_ROLE_KEY | Reads sendo_accounts, schema and storage; accounts add inserts with it. |
SENDO_STORAGE_BUCKET | Bucket to check. Default sendo-media. |
<token_ref> | One per account, e.g. SENDO_TOKEN_MAIN. Must be set locally for doctor and accounts add. |
SENDO_META_APP_ID | Optional. When set, doctor also checks this app is in the WABA's subscribed apps. |
SENDO_CONNECT_API_KEY, SENDO_CONNECT_SIGNING_SECRET | Sendo Connect: when either is set both are required and the connect:* checks run. |
SENDO_CONNECT_BASE_URL | Optional. Default https://connect.sendo.quami.dev. |
doctor#
npx @quami/sendo doctor
npx @quami/sendo doctor --account 123456789012345 # one account (id or phone_number_id)
npx @quami/sendo doctor --url https://<app>/api/sendo/webhook # also test the webhook challenge
npx @quami/sendo doctor --json # machine-readable resultsPrints a pass/fail table with a fix hint under each failure. Checks never throw: a broken step is reported and the rest still run. Each failure line is FAIL, followed by -> Fix: and a docs link. Warnings (WARN, warning: true in --json) carry a fix too but don't change the exit code.
doctor checks#
| id | Passes when | Typical fix |
|---|---|---|
env | SENDO_META_APP_SECRET, SENDO_VERIFY_TOKEN, Supabase URL + service role key and every account's token_ref are set. | Add them to .env.local / the deployment. |
tables | All sendo_* tables exist. | Apply the schema migration. |
schema-version | sendo_schema_meta matches the package (SENDO_SCHEMA_VERSION). | Apply the latest migration. |
storage | The SENDO_STORAGE_BUCKET bucket exists. | Create a private bucket. |
accounts | At least one active sendo_accounts row. | npx @quami/sendo accounts add |
account:<phone_number_id>:token | Reported (failing) only when the token env var is empty. | Set the variable named by token_ref. |
account:<phone_number_id>:phone | GET /{phone_number_id} works with the token; shows verification status and quality rating. RED quality fails. | Token expired (190) or wrong phone_number_id. |
account:<phone_number_id>:subscription | GET /{waba_id}/subscribed_apps lists at least one app (and SENDO_META_APP_ID, if set). | POST /{waba_id}/subscribed_apps; subscribe the messages field. |
account:<phone_number_id>:templates | Templates can be listed from Meta (counts approved ones). | Token needs whatsapp_business_management. |
account:<phone_number_id>:billing | The stored billingReady is true. false or unknown (null) is a warning, not a failure. | Add a payment method in Meta Business Settings, then sendo.accounts.refreshBilling(id) (Billing state). |
webhook-url | Only with --url: the GET verify challenge echoes the challenge with SENDO_VERIFY_TOKEN. | Deploy the route, align SENDO_VERIFY_TOKEN. |
connect:key, connect:webhook, connect:delivery | Only with the Connect env vars: Connect accepts the key, has your webhook URL registered (equal to --url, if given) and its last delivery got a 2xx (none yet is a warning). | See Sendo Connect. |
The template check lists templates from Meta; it doesn’t write sendo_templates. Your app populates that table with sendo.templates.sync().
accounts add#
# interactive (prompts for anything missing)
npx @quami/sendo accounts add
# non-interactive
npx @quami/sendo accounts add --phone-number-id 123456789012345 --token-env SENDO_TOKEN_MAIN \
--waba-id 987654321098765 --tenant-id <uuid> --display-phone "+502 5555 1234" \
--timezone America/Guatemala --yes| Flag | Notes |
|---|---|
--phone-number-id | Required. Meta Phone number ID. |
--token-env | Required. Name of the env var with the token; it must be set and non-empty, or the command stops before calling Meta. |
--waba-id | Optional. Looked up from the token's debug_token scopes when there's a single candidate; otherwise asked for. |
--display-phone | Optional. Fetched from Meta by default. |
--tenant-id | Optional uuid. Default: no tenant (null). |
--timezone | Optional IANA zone. Default America/Guatemala. |
--yes, -y | No prompts; fail if a required value is missing. Prompts only happen on a TTY. |
It inserts the row through the Supabase adapter and stores only the env var name in token_ref. An existing phone_number_id fails with DuplicateAccountError. To change a row later, use the commands below or sendo.accounts.
accounts list, set-default, disconnect#
npx @quami/sendo accounts list # every number, connected or not
npx @quami/sendo accounts list --tenant-id <uuid> --json
npx @quami/sendo accounts set-default 123456789012345 # id or phone_number_id
npx @quami/sendo accounts disconnect 123456789012345listprints id, number, label, tenant, status (connected / disconnected), whether it is the tenant’s default and how it was onboarded.--jsonprints theAccountobjects.set-defaultmakes the number its tenant’s default (atomic swap). A disconnected number exits 1.disconnectdoes whatsendo.accounts.disconnectdoes, except that the CLI has nodeleteTokenhook: it reminds you to delete the token yourself. For Embedded Signup numbers it unsubscribes the app from the WABA with the token in the env var named bytoken_ref, when it is set.
accounts export#
npx @quami/sendo accounts export 123456789012345 --out history.json # id or phone_number_id
npx @quami/sendo accounts export 123456789012345 > history.jsonWrites the number's whole history, connected or disconnected, as one JSON document: the same v1 export as sendo.accounts.export (contacts, tags, conversations with every message, internal note, assignment and system event, and media references with URLs signed for 7 days). Without --out the document goes to stdout; progress and the summary go to stderr, so redirecting stdout gives a clean file. With --out it writes <file>.partial and renames it when done, so a failed export leaves no file. The export streams page by page and is a snapshot as of when it started. Like the other accounts commands it uses the Supabase adapter from env and trusts the operator (no actor). Deleting the history is a separate step: purge.
rules sync#
Stores the rules you declare in code in sendo_rules, where the rules screen and the engine read them.
npx @quami/sendo rules sync --config lib/sendo-rules.ts --tenant <uuid>
npx @quami/sendo rules sync --config lib/sendo-rules.ts --tenant <uuid> --force--configloads a JS or TS module (throughjiti, CLI only) that exportsrules: RuleDefinition[]. Keep the rules in a standalone file such aslib/sendo-rules.tsand import it fromlib/sendo.tstoo, so the CLI never loads Next.js-only code.- Every rule is validated with the package's zod schemas first; one invalid rule exits 1 and nothing is written.
- Rules are inserted by
(tenant, key)with origincode. A rule that already exists (for example, edited on the rules screen) is left as is unless you pass--force, which replaces its type, params and account; its position and on/off state stay as the screen left them. --tenantis the tenant of rules that don't declaretenant; rules declaring another tenant are skipped. A rule'saccountmay be the sendo account id or the phone number id.
mcp#
sendo mcp runs a stdio MCP server for coding agents (docs search, API reference, registry items with their AGENTS.md, read-only doctor). You don't run it by hand: register npx -y @quami/sendo mcp in Claude Code or Cursor, see Para agentes.
--config <path> (doctor and mcp) loads a JS or TS module that exports adapter, for apps whose storage isn't the Supabase adapter built from env (see run_doctor and your adapter).
Exit codes#
| Code | Meaning |
|---|---|
| 0 | Every check passed / the accounts or rules command worked. |
| 1 | At least one check failed (warnings don't count), or the command failed. |
| 2 | Usage error (unknown command or flag). |
Programmatic use#
runDoctor, doctorExitCode, addAccount and findWabaIdsForToken are exported from @quami/sendo:
import { createGraphClient, doctorExitCode, runDoctor } from "@quami/sendo";
import { memoryAdapter } from "@quami/sendo/adapters/memory";
// The CLI is a thin wrapper: run the same checks from a test or a health endpoint.
const results = await runDoctor({
env: process.env,
adapter: memoryAdapter(), // or supabaseAdapter(...); null when it can't be built
graph: createGraphClient(),
url: "https://example.com/api/sendo/webhook",
});
for (const { id, ok, detail, hint } of results) {
console.log(ok ? "✓" : "✗", id, detail, hint ?? "");
}
process.exitCode = doctorExitCode(results); // 0 or 1- Each result is
{ id, label, ok, detail, fix?, hint?, warning? };detail,fixandhintnever contain secrets.warning: truemarks a failing check that doesn't fail the run. - Setup walkthrough: Quickstart step 8.