Send, receive, auto-reply, and inspect WhatsApp messages over Twilio or your personal WhatsApp Web session. Ships with a one-command webhook setup (Tailscale Funnel + Twilio callback) and a configurable auto-reply engine (plain text or command/Claude driven).
Install from npm (global): npm install -g warelay (Node 22+). Then choose one path:
A) Personal WhatsApp Web (preferred: no Twilio creds, fastest setup)
- Link your account:
warelay login(scan the QR). - Send a message:
warelay send --to +12345550000 --message "Hi from warelay"(add--provider webif you want to force the web session). - Stay online & auto-reply:
warelay relay --verbose(defaults to Web when logged in, falls back to Twilio otherwise).
B) Twilio WhatsApp number (for delivery status + webhooks)
- Copy
.env.example→.env; setTWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKENorTWILIO_API_KEY/TWILIO_API_SECRET, andTWILIO_WHATSAPP_FROM=whatsapp:+19995550123(optionalTWILIO_SENDER_SID). - Send a message:
warelay send --to +12345550000 --message "Hi from warelay". - Receive replies:
- Polling (no ingress):
warelay relay --provider twilio --interval 5 --lookback 10 - Webhook + public URL via Tailscale Funnel:
warelay webhook --ingress tailscale --port 42873 --path /webhook/whatsapp --verbose
- Polling (no ingress):
Already developing locally? You can still run
pnpm installandpnpm warelay ...from the repo, but end users only need the npm package.
- Two providers: Twilio (default) for reliable delivery + status; Web provider for quick personal sends/receives via QR login.
- Auto-replies: Static templates or external commands (Claude-aware), with per-sender or global sessions and
/newresets. - Claude setup guide: see
docs/claude-config.mdfor the exact Claude CLI configuration we support. - Webhook in one go:
warelay webhook --ingress tailscaleenables Tailscale Funnel, runs the webhook server, and updates the Twilio sender callback URL. - Polling fallback:
relaypolls Twilio when webhooks aren’t available; works headless. - Status + delivery tracking:
statusshows recent inbound/outbound;sendcan wait for final Twilio status.
| Command | What it does | Core flags |
|---|---|---|
warelay send |
Send a WhatsApp message (Twilio or Web) | --to <e164> --message <text> --wait <sec> --poll <sec> --provider twilio|web --json --dry-run --verbose |
warelay relay |
Auto-reply loop (poll Twilio or listen on Web) | --provider <auto|twilio|web> --interval <sec> --lookback <min> --verbose |
warelay status |
Show recent sent/received messages | --limit <n> --lookback <min> --json --verbose |
warelay webhook |
Run inbound webhook (ingress=tailscale updates Twilio; none is local-only) |
--ingress tailscale|none --port <port> --path <path> --reply <text> --verbose --yes --dry-run |
warelay login |
Link personal WhatsApp Web via QR | --verbose |
- Twilio:
warelay send --to +1... --message "Hi" --media ./pic.jpg --serve-media(needswarelay webhook --ingress tailscaleor--serve-mediato auto-host via Funnel; max 5 MB). - Web:
warelay send --provider web --media ./pic.jpg --message "Hi"(local path or URL; no hosting needed). - Auto-replies can attach
mediaUrlin~/.warelay/warelay.json(used alongsidetextwhen present).
- Twilio (default): needs
.envcreds + WhatsApp-enabled number; supports delivery tracking, polling, webhooks, and auto-reply typing indicators. - Web (
--provider web): uses your personal WhatsApp via Baileys; supports send/receive + auto-reply, but no delivery-status wait; cache lives in~/.warelay/credentials/(rerunloginif logged out). - Auto-select (
relayonly):--provider autouses Web when logged in, otherwise Twilio polling.
Best practice: use a dedicated WhatsApp account (separate SIM/eSIM or business account) for automation instead of your primary personal account to avoid unexpected logouts or rate limits.
| Variable | Required | Description |
|---|---|---|
TWILIO_ACCOUNT_SID |
Yes (Twilio provider) | Twilio Account SID |
TWILIO_AUTH_TOKEN |
Yes* | Auth token (or use API key/secret) |
TWILIO_API_KEY |
Yes* | API key if not using auth token |
TWILIO_API_SECRET |
Yes* | API secret paired with TWILIO_API_KEY |
TWILIO_WHATSAPP_FROM |
Yes (Twilio provider) | WhatsApp-enabled sender, e.g. whatsapp:+19995550123 |
TWILIO_SENDER_SID |
Optional | Overrides auto-discovery of the sender SID |
(*Provide either auth token OR api key/secret.)
- Controls who is allowed to trigger replies (
allowFrom), reply mode (textorcommand), templates, and session behavior. - Example (Claude command):
{
inbound: {
allowFrom: ["+12345550000"],
reply: {
mode: "command",
bodyPrefix: "You are a concise WhatsApp assistant.\n\n",
command: ["claude", "--dangerously-skip-permissions", "{{BodyStripped}}"],
claudeOutputFormat: "text",
session: { scope: "per-sender", resetTriggers: ["/new"], idleMinutes: 60 }
}
}
}- Install the official Claude CLI (e.g.,
brew install anthropic-ai/cli/claudeor follow the Anthropic docs) and runclaude loginso it can read your API key. - In
warelay.json, setreply.modeto"command"and pointcommand[0]to"claude"; setclaudeOutputFormatto"text"(or"json"/"stream-json"if you want warelay to parse and trim the JSON output). - (Optional) Add
bodyPrefixto inject a system prompt andsessionsettings to keep multi-turn context (/newresets by default). - Run
pnpm warelay relay --provider auto(or--provider web|twilio) and send a WhatsApp message; warelay will queue the Claude call, stream typing indicators (Twilio provider), parse the result, and send back the text.
| Key | Type & default | Notes |
|---|---|---|
inbound.allowFrom |
string[] (default: empty) |
E.164 numbers allowed to trigger auto-reply (no whatsapp:). |
inbound.reply.mode |
"text" | "command" (default: —) |
Reply style. |
inbound.reply.text |
string (default: —) |
Used when mode=text; templating supported. |
inbound.reply.command |
string[] (default: —) |
Argv for mode=command; each element templated. Stdout (trimmed) is sent. |
inbound.reply.template |
string (default: —) |
Injected as argv[1] (prompt prefix) before the body. |
inbound.reply.bodyPrefix |
string (default: —) |
Prepended to Body before templating (great for system prompts). |
inbound.reply.timeoutSeconds |
number (default: 600) |
Command timeout. |
inbound.reply.claudeOutputFormat |
"text"|"json"|"stream-json" (default: —) |
When command starts with claude, auto-adds --output-format + -p/--print and trims reply text. |
inbound.reply.session.scope |
"per-sender"|"global" (default: per-sender) |
Session bucket for conversation memory. |
inbound.reply.session.resetTriggers |
string[] (default: ["/new"]) |
Exact match or prefix (/new hi) resets session. |
inbound.reply.session.idleMinutes |
number (default: 60) |
Session expires after idle period. |
inbound.reply.session.store |
string (default: ~/.warelay/sessions.json) |
Custom session store path. |
inbound.reply.session.sessionArgNew |
string[] (default: ["--session-id","{{SessionId}}"]) |
Args injected for a new session run. |
inbound.reply.session.sessionArgResume |
string[] (default: ["--resume","{{SessionId}}"]) |
Args for resumed sessions. |
inbound.reply.session.sessionArgBeforeBody |
boolean (default: true) |
Place session args before final body arg. |
Templating tokens: {{Body}}, {{BodyStripped}}, {{From}}, {{To}}, {{MessageSid}}, plus {{SessionId}} and {{IsNewSession}} when sessions are enabled.
warelay webhook --ingress nonestarts the local Express server on your chosen port/path; add--reply "Got it"for a static reply when no config file is present.warelay webhook --ingress tailscaleenables Tailscale Funnel, prints the public URL (https://<tailnet-host><path>), starts the webhook, discovers the WhatsApp sender SID, and updates Twilio callbacks to the Funnel URL.- If Funnel is not allowed on your tailnet, the CLI exits with guidance; you can still use
relay --provider twilioto poll without webhooks.
- Send/receive issues: run
pnpm warelay status --limit 20 --lookback 240 --jsonto inspect recent traffic. - Auto-reply not firing: ensure sender is in
allowFrom(or unset), and confirm.env+warelay.jsonare loaded (reload shell after edits). - Web provider dropped: rerun
pnpm warelay login; credentials live in~/.warelay/credentials/. - Tailscale Funnel errors: update tailscale/tailscaled; check admin console that Funnel is enabled for this device.
- Twilio errors: 63016 “permission to send an SMS has not been enabled” → ensure your number is WhatsApp-enabled; 63007 template not approved → send a free-form session message within 24h or use an approved template; 63112 policy violation → adjust content, shorten to <1600 chars, avoid links that trigger spam filters. Re-run
pnpm warelay statusto see the exact Twilio response body. - Does this store my messages? warelay only writes
~/.warelay/warelay.json(config),~/.warelay/credentials/(WhatsApp Web auth), and~/.warelay/sessions.json(session IDs + timestamps). It does not persist message bodies beyond the session store. Logs print to stdout/stderr; redirect or rotate if needed. - Personal WhatsApp safety: Automation on personal accounts can be rate-limited or logged out by WhatsApp. Use
--provider websparingly, keep messages human-like, and re-runloginif the session is dropped. - Limits to remember: WhatsApp text limit ~1600 chars; avoid rapid bursts—space sends by a few seconds; keep webhook replies under a couple seconds for good UX; command auto-replies time out after 600s by default.
- Deploy / keep running: Use
tmuxorscreenfor ad-hoc (tmux new -s warelay -- pnpm warelay relay --provider twilio). For long-running hosts, wrappnpm warelay relay ...orpnpm warelay webhook --ingress tailscale ...in a systemd service or macOS LaunchAgent; ensure environment variables are loaded in that context. - Rotating credentials: Update
.env(Twilio keys), rerun your process; for Web provider, delete~/.warelay/credentials/and rerunpnpm warelay loginto relink.