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.

usage
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 | --version

Environment#

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.

VariableUsed for
SENDO_META_APP_SECRETRequired (env check), unless the Connect variables are set.
SENDO_VERIFY_TOKENRequired; also used for the --url challenge.
SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URLSupabase project URL.
SUPABASE_SERVICE_ROLE_KEYReads sendo_accounts, schema and storage; accounts add inserts with it.
SENDO_STORAGE_BUCKETBucket 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_IDOptional. When set, doctor also checks this app is in the WABA's subscribed apps.
SENDO_CONNECT_API_KEY, SENDO_CONNECT_SIGNING_SECRETSendo Connect: when either is set both are required and the connect:* checks run.
SENDO_CONNECT_BASE_URLOptional. Default https://connect.sendo.quami.dev.

doctor#

shell
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 results

Prints 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#

idPasses whenTypical fix
envSENDO_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.
tablesAll sendo_* tables exist.Apply the schema migration.
schema-versionsendo_schema_meta matches the package (SENDO_SCHEMA_VERSION).Apply the latest migration.
storageThe SENDO_STORAGE_BUCKET bucket exists.Create a private bucket.
accountsAt least one active sendo_accounts row.npx @quami/sendo accounts add
account:<phone_number_id>:tokenReported (failing) only when the token env var is empty.Set the variable named by token_ref.
account:<phone_number_id>:phoneGET /{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>:subscriptionGET /{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>:templatesTemplates can be listed from Meta (counts approved ones).Token needs whatsapp_business_management.
account:<phone_number_id>:billingThe 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-urlOnly 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:deliveryOnly 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#

shell
# 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
FlagNotes
--phone-number-idRequired. Meta Phone number ID.
--token-envRequired. Name of the env var with the token; it must be set and non-empty, or the command stops before calling Meta.
--waba-idOptional. Looked up from the token's debug_token scopes when there's a single candidate; otherwise asked for.
--display-phoneOptional. Fetched from Meta by default.
--tenant-idOptional uuid. Default: no tenant (null).
--timezoneOptional IANA zone. Default America/Guatemala.
--yes, -yNo 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#

shell
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 123456789012345
  • list prints id, number, label, tenant, status (connected / disconnected), whether it is the tenant’s default and how it was onboarded. --json prints the Account objects.
  • set-default makes the number its tenant’s default (atomic swap). A disconnected number exits 1.
  • disconnect does what sendo.accounts.disconnect does, except that the CLI has no deleteToken hook: 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 by token_ref, when it is set.

accounts export#

shell
npx @quami/sendo accounts export 123456789012345 --out history.json   # id or phone_number_id
npx @quami/sendo accounts export 123456789012345 > history.json

Writes 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.

shell
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
  • --config loads a JS or TS module (through jiti, CLI only) that exports rules: RuleDefinition[]. Keep the rules in a standalone file such as lib/sendo-rules.ts and import it from lib/sendo.ts too, 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 origin code. 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.
  • --tenant is the tenant of rules that don't declare tenant; rules declaring another tenant are skipped. A rule's account may 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#

CodeMeaning
0Every check passed / the accounts or rules command worked.
1At least one check failed (warnings don't count), or the command failed.
2Usage error (unknown command or flag).

Programmatic use#

runDoctor, doctorExitCode, addAccount and findWabaIdsForToken are exported from @quami/sendo:

ts
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, fix and hint never contain secrets. warning: true marks a failing check that doesn't fail the run.
  • Setup walkthrough: Quickstart step 8.