WhatsApp Calling
The calling command mirrors the /calling page and call is its
per-id sibling. Together they cover everything the page does except
the browser-only WebRTC live-audio bridge (intentionally — it’s a real-time
audio stream that has no place in a CLI).
Backed by /api/v1/app/calling/*. Like broadcasts and email, every send
runs a subscription + wallet preflight before submission.
Quick start
splashify calling # overview card (page header)
splashify calling analytics # period analytics
splashify calling calls --limit 50 # recent calls
# Templates
splashify calling templates # list
splashify calling template create-call-button … # tap-to-call (URL/PHONE)
splashify calling template create-permission … # permission request
splashify calling send <template_id> --to +91… # send a template message
# Backend-initiated outbound call
splashify calling initiate --to +91… --phone-number-id <id>
# Recordings
splashify calling upload-recording <file> --call-id <id>Command reference
Overview, settings, analytics, history
splashify calling # page-header overview
splashify calling overview # alias
splashify calling settings # business hours + icon visibility
splashify calling settings update --hours … --icon-visible true
splashify calling analytics [--period 30d|7d|today]
splashify calling calls [--limit N] [--status sent|delivered|failed]
splashify calling call <call_id> # detail (alias: splashify call <id>)
splashify calling call <call_id> status # latest status from backend| Backed by | GET /app/calling/overview |
|---|---|
GET/PUT /app/calling/settings | |
GET /app/calling/analytics?period=… | |
GET /app/calling/calls?limit=&status= | |
GET /app/calling/calls/:call_id | |
GET /app/calling/calls/:call_id/status |
Templates — call buttons & permissions
WhatsApp Calling has two template variants:
| Type | Purpose | Backend |
|---|---|---|
| Call button | Adds a tap-to-call CTA on outbound messages | POST /app/calling/templates/call-button |
| Permission | Asks the recipient to grant calling permission first | POST /app/calling/templates/permission |
splashify calling templates # list all
splashify calling template <template_id> # show one
splashify calling template create-call-button \
--name "support_cb" --label "Call support" --phone-number +919876543210
splashify calling template create-permission \
--name "ask_perm" --body "Tap to allow us to call you"
splashify calling template set-default <template_id> # default for sends
splashify calling template delete <template_id>| Backed by | GET/DELETE /app/calling/templates[/:id] |
|---|---|
POST /app/calling/templates/call-button | |
POST /app/calling/templates/permission | |
POST /app/calling/templates/:id/set-default |
Send a calling template message
splashify calling send <template_id> --to +919876543210
splashify calling send <template_id> --to +919876543210 --vars '["John","ORD-1024"]'| Backed by | POST /app/calling/send |
|---|
Same preflight as broadcasts/email — refuses on subscription_expired
or insufficient_balance before round-trip. Calling has its own per-call
rate (separate from text + media).
Initiate an outbound call (AI-agent bridge)
splashify call initiate --to "+919876543210" # uses your default voice agent
splashify call initiate --to "+919876543210" --agent-id <uuid> # override per-call| Backed by | POST /app/calling/initiate → 202 Accepted |
|---|---|
| Status | GET /app/calling/initiate/:pending_call_id |
WhatsApp Business Calling is peer-to-peer WebRTC — the business side
must be a real audio peer. A shell can’t be that peer, so
wa-call-bridge does it for you:
- The handler returns
202with apending_call_idand queues the request on Rediswa-call:outgoing. - The bridge spins up a Pion WebRTC peer, generates the SDP offer,
posts to Meta
/calls action=connect, gets themeta_call_id. - Meta delivers the SDP answer via webhook → bridge applies it → recipient’s WhatsApp rings.
- The bridge joins a LiveKit room named
wa-call-<meta_call_id>,voice-agent-workerauto-dispatches your AI agent into the room, and the agent speaks.
# Watch a call progress
PID=$(splashify call initiate --to "+919876543210" | jq -r '.pending_call_id')
while : ; do
STATE=$(splashify call status "$PID" | jq -r '.state')
echo "$(date -u +%H:%M:%S) $STATE"
case "$STATE" in
connected|ended|failed_*) break ;;
esac
sleep 2
doneThe state field follows this lifecycle:
| State | Meaning |
|---|---|
queued | Handler accepted; bridge hasn’t picked up yet. |
dialing | Bridge built the SDP offer; POSTing to Meta. |
awaiting_answer | Meta accepted; waiting for the answer webhook. |
connecting | Answer applied; ICE candidates flowing. |
connected | Recipient picked up; agent in LiveKit room. |
ended | Call finished normally. |
failed_no_agent | No default_voice_ai_agent_id and no --agent-id. |
failed_no_answer | Meta never delivered the SDP answer (~30s timeout). |
failed_meta | Meta API rejected the connect POST. |
failed_pion | Bridge couldn’t build the WebRTC peer. |
AI-agent only. Outbound CLI calls always route audio to your default voice agent (or
--agent-id). The CLI can’t carry audio itself — there’s no terminal-side mic/speaker pipe. To make a human outbound call, use the dialer at/messages.
Permission lookup
splashify calling permission-status --to +919876543210 # has this user granted calling perm?
splashify calling permissions # all granted permissions| Backed by | GET /app/calling/permission-status?phone=… |
|---|---|
GET /app/calling/permissions |
Upload a recording
splashify calling upload-recording ./call.mp3 --call-id <call_id>| Backed by | POST /app/calling/calls/:call_id/recording (multipart) |
|---|
Uploads a recording to attach to a call row — useful when the audio
came from a third-party PBX and you want it logged in /calling/<id>.
Common workflows
Daily call volume report
splashify calling analytics --period 7d | \
jq '.analytics.daily_series[] | {date, total: .count, failed: .failed_count}'Re-send the default call-button template to every contact tagged “leads”
TPL=$(splashify calling templates | jq -r '.templates[] | select(.is_default) | .template_id')
splashify contacts --tag leads | jq -r '.contacts[].phone_number' | \
xargs -I{} splashify calling send "$TPL" --to {}Watch a call ring through
See the “Initiate an outbound call” section above — the polling loop is
on splashify call status <pending_call_id>, not the legacy
splashify calling call <id> status.
Pause calling outside business hours
splashify calling settings update \
--hours '{"mon":["09:00","18:00"],"tue":["09:00","18:00"], … }' \
--icon-visible trueTroubleshooting
permission_required on send — the recipient hasn’t accepted the
permission template. Send a --type permission template first (or check
with splashify calling permission-status).
subscription_expired / insufficient_balance — same preflight
errors as broadcasts/email. See Subscription
and Wallet.
initiate returns 409 — no default voice agent — outbound CLI
calls route audio to your AI agent. Either set a default voice agent
at /ai-agents (it lights up
app_users.default_voice_ai_agent_id) or pass --agent-id <uuid>
on the command.
failed_no_answer after ~30s — Meta accepted the connect POST but
never delivered the SDP answer webhook. Common causes: recipient
hasn’t granted call permission yet (check splashify calling permission-status), the WhatsApp number’s calling status isn’t
ENABLED in the calling settings, or the call hours window is closed.
failed_meta on initiate — surface meta_error from
splashify call status to see the verbatim Meta response. Most often
this is a stale Interakt token or the WABA phone number isn’t
registered for Business Calling.
Upload-recording rejects the file — Meta accepts MP3/WAV up to ~25MB. Larger files / other formats are refused with a 400 listing the limits.
No browser dialer from the CLI — by design. CLI-initiated outbound
calls always speak through your AI voice agent. To make a human
outbound call, use the dialer at /messages.
See also
splashify templates— non-calling templates (text, media, interactive).splashify expenses— per-call billing log.splashify message— the parallel surface for non-calling WhatsApp.