Skip to Content
splashify CLIWhatsApp Calling

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 byGET /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:

TypePurposeBackend
Call buttonAdds a tap-to-call CTA on outbound messagesPOST /app/calling/templates/call-button
PermissionAsks the recipient to grant calling permission firstPOST /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 byGET/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 byPOST /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 byPOST /app/calling/initiate → 202 Accepted
StatusGET /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:

  1. The handler returns 202 with a pending_call_id and queues the request on Redis wa-call:outgoing.
  2. The bridge spins up a Pion WebRTC peer, generates the SDP offer, posts to Meta /calls action=connect, gets the meta_call_id.
  3. Meta delivers the SDP answer via webhook → bridge applies it → recipient’s WhatsApp rings.
  4. The bridge joins a LiveKit room named wa-call-<meta_call_id>, voice-agent-worker auto-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 done

The state field follows this lifecycle:

StateMeaning
queuedHandler accepted; bridge hasn’t picked up yet.
dialingBridge built the SDP offer; POSTing to Meta.
awaiting_answerMeta accepted; waiting for the answer webhook.
connectingAnswer applied; ICE candidates flowing.
connectedRecipient picked up; agent in LiveKit room.
endedCall finished normally.
failed_no_agentNo default_voice_ai_agent_id and no --agent-id.
failed_no_answerMeta never delivered the SDP answer (~30s timeout).
failed_metaMeta API rejected the connect POST.
failed_pionBridge 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 byGET /app/calling/permission-status?phone=…
GET /app/calling/permissions

Upload a recording

splashify calling upload-recording ./call.mp3 --call-id <call_id>
Backed byPOST /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 true

Troubleshooting

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