Skip to Content

splashify CLI Commands

The splashify CLI controls your Splashify Pro account from the command line. Every command authenticates with your stored oc_live_ access token and prints the backend’s JSON response.

The CLI performs every app task directly — sending messages, managing contacts, running broadcasts, and more. It can also install the Splashify OpenClaw skill for you, but that is optional: the CLI is fully usable on its own.

Setup commands

CommandDescription
splashify connectConnect this machine — prompts for the backend URL and an oc_live_ token
splashify whoamiShow the connected account
splashify doctorDiagnose config, token, OpenClaw, and the Splashify skill
splashify versionPrint the CLI version
splashify helpShow the full command list

Configuration is stored at ~/.splashify/config.json (mode 0600).

Access token commands

CommandDescription
splashify token listList your access tokens
splashify token create --name "<name>" [--expires-days N]Create a new token
splashify token revoke <id>Revoke a token immediately

See Access Tokens for details.

OpenClaw skill command

CommandDescription
splashify link openclaw [--print] [--path <dir>]Install the Splashify skill into OpenClaw’s workspace skills directory

splashify link openclaw writes the skill bundle (embedded in the CLI binary) into ~/.openclaw/workspace/skills/splashify/. Use --print to preview the target path, or --path to override it. This is the only OpenClaw-related command — there is no MCP server to configure. See the OpenClaw skill section for the full integration guide.

Messaging commands

# Send a free-form text message (within the 24-hour window) splashify message send --to +919876543210 --text "Your order has shipped" # Send an approved WhatsApp template splashify message template --to +919876543210 --name order_update \ --lang en --vars '["John","ORD-1024"]' # Send a media message splashify message media --to +919876543210 --type image \ --url https://example.com/receipt.png --caption "Receipt" # Conversations splashify conversations --status open # list chats splashify conversation <id> # read one chat splashify conversation <id> resolve # close a chat splashify unread # unread message count
CommandDescription
message send --to --textSend a text message
message template --to --name [--lang] [--vars]Send a template message
message media --to --type --url [--caption]Send image/document/video/audio
conversations [--page] [--page-size] [--status]List conversations
conversation <id> [resolve]Read or resolve a conversation
unreadUnread message count

Contact commands

splashify contacts --search john --tag vip splashify contact <id> splashify contact create --phone +919876543210 --name "John Roe" --email john@x.com splashify contact update <id> --notes "VIP. Prefers WhatsApp." splashify contact tag <id> --tags vip,lead splashify contact untag <id> --tags lead splashify contact block <id> splashify contact unblock <id> splashify contact delete <id>

Full guide: Contacts — covers sparse-PUT update semantics, tag/untag vs. update, block/unblock side-effects, and bulk patterns.

Account commands (read-only)

The account command mirrors everything the Settings → Account Details page shows in the app — profile, plan, wallet balance, WhatsApp identifiers, the organizations you belong to, and your invitations (received and sent). Available since v0.1.4.

Read-only by design. None of these subcommands accept, reject, switch, invite, or update anything. To change account state, use the web app.

splashify account # consolidated view (all sections at once) splashify account info # profile + plan (from /app/me) splashify account orgs # organizations you belong to splashify account invitations # pending invitations received splashify account sent-invitations # invitations you have sent splashify account wallet # wallet balance
SubcommandBacked by
(none — default)Merges all six endpoints below into one JSON object
info / me / profileGET /api/v1/app/me
orgs / organizationsGET /api/v1/app/organizations
invitations / invitesGET /api/v1/app/org/invitations
sent-invitations / sent-invitesGET /api/v1/app/org/sent-invitations
walletGET /api/v1/app/wallet/info

The default consolidated view returns:

{ "user": { ... profile, plan, dates ... }, "wallet": { "wallet_amount": 1234.56, ... }, "whatsapp": { "phone_number": "+91...", "waba_id": "...", ... }, "organizations": { "organizations": [ ... ] }, "invitations_received": { "invitations": [ ... ] }, "invitations_sent": { "invitations": [ ... ] } }

A section that fails for any reason is reported as {"error": "..."} for that key — the command never aborts the whole view because one part is unavailable.

Common one-liners with jq:

# Just the things that matter day-to-day splashify account | jq '{ email: .user.user.email, plan: .user.user.plan_name, plan_status: .user.user.plan_status, expires: .user.user.plan_expires_at, wallet: .wallet.wallet_amount, waba_phone: .whatsapp.phone_number, org_count: (.organizations.organizations | length), pending_invs: (.invitations_received.invitations | length) }' # Org IDs only splashify account orgs | jq -r '.organizations[].org_user_id' # Pending invitations summary splashify account invitations | jq '.invitations[] | {from: .inviter_org, role, expires: .expires_at}'

Support tickets

Full mirror of /support and /support/<ticket_id> — list and filter your tickets, file a new one, follow up with a reply, or close it. Available since v0.1.25. No subscription / balance preflight — support is free for every account.

splashify support # list every ticket splashify support list --status open # status filter splashify support list --search "payment" # subject substring splashify tickets # alias for support splashify ticket <id> # detail with replies splashify ticket create \ --title "Cannot send broadcast to segment X" \ --description "When I try to send to segment X I get a 402…" \ --category bug \ --priority high splashify ticket <id> reply "Thanks — the workaround worked, please close." splashify ticket <id> close
SubcommandEndpoint
support / support list [--status --search] / ticketsGET /api/v1/app/tickets (filters are client-side, same as the page)
ticket <id>GET /api/v1/app/tickets/:ticket_id
ticket create --title --description --category [--priority]POST /api/v1/app/tickets
ticket <id> reply "<message>"POST /api/v1/app/tickets/:ticket_id/replies
ticket <id> closePOST /api/v1/app/tickets/:ticket_id/close

Categories and priorities (match the page’s dialogs)

--categoryWhat it means
bugSomething isn’t working
billingPayments & subscriptions
feature_requestSuggest an improvement
accountLogin, profile & settings
generalAnything else
--priorityWhen to use
lowCosmetic / non-blocking
mediumDefault; affects a workflow but has a workaround
highAffects a workflow, no workaround
urgentProduction outage / billing-blocker

Statuses you’ll see (read-only)

StatusMeaning
openNew, not yet picked up
in_progressSupport team is working on it
waiting_for_userSupport replied; reply needed from you
resolvedMarked resolved by support
closedClosed (by you or after resolution)

Reply syntax shortcut

reply joins everything after it with spaces, so you don’t have to quote a short message:

splashify ticket abc reply Thanks for the update # is equivalent to splashify ticket abc reply "Thanks for the update"

For multi-line replies, pass an explicit quoted string with \n and let the shell expand it (or echo into a heredoc piped into splashify api).

Common patterns

# Tickets that need a reply from me (the violet "Reply needed" pills) splashify support list --status waiting_for_user | jq '.tickets[]' # Open + in-progress count splashify support | jq '.tickets | group_by(.status) | map({(.[0].status): length}) | add' # File a quick bug report with subject + body splashify ticket create \ --title "Broadcast 405de606 stuck in 'sending'" \ --description "Status hasn't changed for 30 minutes. cohort-counts shows 12 failed and 0 sent." \ --category bug --priority high # Latest unresolved ticket splashify support | \ jq '.tickets | map(select(.status != "resolved" and .status != "closed")) | sort_by(.updated_at) | reverse | .[0]' # Bulk-close every resolved ticket older than 14 days splashify support list --status resolved | \ jq -r --arg cutoff "$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ)" \ '.tickets[] | select(.updated_at < $cutoff) | .ticket_id' | \ xargs -I{} splashify ticket {} close

Track expenses

Full mirror of /settings/track-expenses — message-deduction summary, daily chart series, and the per-message transaction log, including the page’s Export CSV button. The page is read-only; the CLI is too — no writes, no balance preflight. Available since v0.1.26.

splashify expenses # summary, last 30 days splashify expenses summary --period 7d # summary, last 7 days splashify expenses categories --period 30d # category breakdown only splashify expenses countries --period all # top countries only splashify expenses trends --period 30d # daily deduction series splashify expenses logs --period 30d --limit 200 splashify expenses logs --category marketing --country IN --free-trial false splashify expenses export --period all --out spend.csv
SubcommandEndpoint / behaviour
expenses / expenses summary [--period]GET /api/v1/app/expenses/summary?period=…
expenses categories [--period]Same endpoint, JSON trimmed to category fields
expenses countries [--period]Same endpoint, JSON trimmed to top_countries
expenses trends [--period]GET /api/v1/app/expenses/trends?period=…
expenses logs [--period --limit --category --country --free-trial]GET /api/v1/app/expenses/billing-logs?period=…&limit=… (filters client-side)
expenses export [--period --limit --out]Same endpoint, formatted as the page’s CSV

--period values (same five the page exposes)

ValueRange
7dLast 7 days
30d (default)Last 30 days
3mLast 3 months
6mLast 6 months
allEvery deduction on record

What summary returns

FieldMeaning
total_deductedNet spend in the period (broadcasts settled net of refunds)
total_messagesNet paid message count
marketing_amount / marketing_countPer-bucket roll-up (also utility_*, auth_*, rcs_*, call_*)
avg_per_messagetotal_deducted / total_messages
top_countriesTop 5 destinations by amount ({code, count, amount})
sent_count / delivered_countTrigger-level message counts
free_trial_count / paid_countFree-trial vs. billed split
currencyAlways INR today

logs filters (applied client-side)

The list endpoint takes only period and limit, so additional filters match the page’s UX by filtering the returned page. If your account exceeds 500 deductions in a period, raise --limit 500 (the backend max) or narrow --period.

FlagFilter
--categorymarketing, utility, authentication, rcs, call, broadcast_deduction, broadcast_refund
--countryISO country code, e.g. IN, US, BR
--free-trialtrue (free-trial deductions only) or false (billed only)

CSV export — same columns as the page’s button

splashify expenses export produces a UTF-8 BOM CSV (so Excel renders ₹ correctly) with the seven columns shown by the page’s Export CSV button — no internal base-amount/markup-percent breakdown, just the customer-facing total:

Date, Recipient, Category, Country, Total Deducted, Trigger, Free Trial

Default output is stdout; pass --out file.csv to write to disk.

Common patterns

# Total spend last 7 days splashify expenses summary --period 7d | jq '.summary.total_deducted' # Spend by category, last 30 days splashify expenses categories | \ jq '.summary | {marketing: .marketing_amount, utility: .utility_amount, auth: .auth_amount, rcs: .rcs_amount, call: .call_amount}' # Highest-spend country this period splashify expenses countries --period 30d | jq '.top_countries[0]' # Day with biggest spend in the last 90 days splashify expenses trends --period 3m | jq '.data | max_by(.amount)' # Just the authentication-template deductions to Indian numbers splashify expenses logs --category authentication --country IN --limit 500 # Free-trial messages that didn't actually deduct anything splashify expenses logs --free-trial true | \ jq '.logs | map(select(.total_amount == 0)) | length' # Monthly accounting export splashify expenses export --period 30d --out "spend-$(date +%Y-%m).csv"

Calling

Full mirror of /calling — call history + analytics, “tap-to-call” and permission templates, backend-side dial, recording uploads, and the calling settings (business hours, call-icon visibility, etc.). Available since v0.1.24.

WebRTC live-audio is not exposed. The page’s in-browser call dialog (/app/whatsapp-calling/*) requires real WebRTC SDP/ICE negotiation — a shell can’t drive that. The CLI handles every REST-friendly piece: backend-side dial, template messages that trigger calls, history, analytics, permissions, settings, recording upload.

Subscription + balance preflight

Same preflight as broadcasts. Every command that triggers a charge runs:

  1. GET /app/developer/cli-eligibility → refuses with the documented subscription_expired / no_waba / account_suspended messages.
  2. GET /app/wallet/info → prints balance; refuses when wallet_amount ≤ 0 on commands that initiate a call or send a paid template.

Preflighted commands: call initiate, calling send call-button, calling send permission, calling send template, calling send permission-template. --yes skips the soft balance check; SPLASHIFY_SKIP_PREFLIGHT=1 skips both. Backend remains the source of truth.

Read — overview, calls, analytics, permissions, templates

splashify calling # overview (default) splashify calling overview # same splashify calling settings # GET calling settings splashify calling analytics --start-time 2026-05-01T00:00:00Z \ --end-time 2026-05-20T00:00:00Z \ --granularity DAILY --dimensions DIRECTION splashify calling calls # list call history splashify calling calls --status missed --page 1 --limit 50 splashify calling calls --search "+91987" --agent <agent_id> splashify call <call_id> # one call detail splashify calling permission-status --phone "+919876543210" splashify calling permissions --status granted --agent <agent_id> splashify calling templates # list call templates splashify calling template status --name "call_invitation"
SubcommandEndpoint
calling / calling overviewGET /api/v1/app/calling
calling settingsGET /api/v1/app/calling/settings
calling analytics […]GET /api/v1/app/calling/analytics
calling calls […]GET /api/v1/app/calling/calls
call <call_id>GET /api/v1/app/calling/calls/:call_id
calling permission-status --phone …GET /api/v1/app/calling/permission-status
calling permissions […]GET /api/v1/app/calling/permissions
calling templatesGET /api/v1/app/calling/templates
calling template status --name …GET /api/v1/app/calling/templates/status

Write — initiate, templates, sends, settings update, recording upload

# Backend-side dial (no browser needed — backend places the call via Meta) splashify call initiate --to "+919876543210" # Upload a recording for a previous call (multipart) splashify call upload-recording <call_id> ./recording.webm # Create a "tap to call" template (template containing a call button) splashify calling template create-call-button \ --name "support_call_invite" \ --body-text "Need help? Tap below to call our support team." \ --button-label "Call us" \ --phone-number "+919876543210" \ --language en # Create a permission-request template splashify calling template create-permission \ --name "ask_permission_to_call" \ --body-text "May we call you about your order?" \ --language en # Make a template the default splashify calling template set-default <template_id> # Send a tap-to-call interactive message splashify calling send call-button \ --to "+919876543210" \ --body-text "Want to talk to a human?" \ --button-label "Call us" \ --phone-number "+919876543210" # Send a call permission request splashify calling send permission \ --to "+919876543210" \ --body-text "May we call you about your order?" # Send a permission request as a template (out-of-window safe) splashify calling send permission --to "+919876543210" \ --type template --template '{"name":"ask_permission_to_call","language":{"code":"en"}}' # Send a call-button template (out-of-window safe) splashify calling send template --to "+919876543210" \ --name "support_call_invite" --language en --vars '["Alice"]' # Send a permission-template splashify calling send permission-template \ --to "+919876543210" \ --name "ask_permission_to_call" --language en # Update calling settings (raw JSON; CLI wraps it as {"calling": <your_json>}) splashify calling settings update --data '{ "calling_enabled": true, "business_hours_enabled": true, "call_hours": { "status": "ENABLED", "timezone_id": "Asia/Kolkata", "weekly_operating_hours": [ {"day_of_week":"MONDAY","open_time":"09:00","close_time":"18:00"}, {"day_of_week":"FRIDAY","open_time":"09:00","close_time":"18:00"} ], "holiday_schedule": [] } }'
SubcommandEndpoint
call initiate --to …POST /api/v1/app/calling/initiate
call upload-recording <id> <file>POST /api/v1/app/calling/calls/:call_id/recording (multipart, field recording)
calling template create-call-button …POST /api/v1/app/calling/templates/call-button
calling template create-permission …POST /api/v1/app/calling/templates/permission
calling template set-default <id>POST /api/v1/app/calling/templates/set-default
calling send call-button …POST /api/v1/app/calling/send-call-button
calling send permission …POST /api/v1/app/calling/send-permission
calling send template …POST /api/v1/app/calling/send-template
calling send permission-template …POST /api/v1/app/calling/send-permission-template
calling settings update --data '{…}'PUT /api/v1/app/calling/settings (body wrapped as {"calling": …})

What preflight failure looks like

$ splashify call initiate --to "+919876543210" Wallet balance: ₹0.00 error: calls are unavailable — your wallet balance is ₹0.00. Recharge: https://app.splashifypro.com/dashboard (Or run with --yes to skip this soft-check; the backend still enforces it per-message.)

And for an expired subscription:

error: calls are unavailable — your trial has ended and there is no active paid plan on this account. Upgrade your plan: https://app.splashifypro.com/settings/subscriptions Once a plan is active, retry the command.

End-to-end recipe — “tap-to-call” outreach

# 1. Verify subscription + balance splashify subscription | jq '.eligibility' splashify account wallet | jq '.wallet_amount' # 2. Create a call-button template (Meta must approve it before send) splashify calling template create-call-button \ --name "support_call_invite" \ --body-text "Hi {{1}}, need help? Tap below to call our support team." \ --button-label "Call us" \ --phone-number "+919876543210" \ --language en # 3. Wait for approval, then check status splashify calling template status --name support_call_invite # 4. Once APPROVED, send to a customer splashify calling send template \ --to "+919876543210" \ --name "support_call_invite" \ --language en \ --vars '["Aditya"]' # 5. Customer taps "Call us" → backend logs the call splashify calling calls --status completed --limit 5

Common patterns

# Today's missed calls splashify calling calls --status missed --limit 100 | \ jq '.calls[] | select(.created_at | startswith("2026-05-20"))' # Bulk send a call invite to a list of phone numbers while read phone; do splashify calling send template --to "$phone" \ --name support_call_invite --language en --vars "[\"$phone\"]" done < numbers.txt # Permission status check for a batch while read phone; do status=$(splashify calling permission-status --phone "$phone" | jq -r '.status // "unknown"') echo "$phone $status" done < numbers.txt > permissions.tsv # Backup call history as JSON splashify calling calls --limit 1000 | jq '.calls' > call_history.json

Broadcasts

Full mirror of /broadcasts (the list + stats) and /broadcasts/<id> (detail, per-message status, cohort counts, export, cancel / restart / send-now / rebroadcast). Rebuilt in v0.1.23 — earlier versions had a broken stub that used the wrong JSON field names.

Subscription + balance preflight

Every command that triggers actual sends runs two checks first:

  1. SubscriptionGET /api/v1/app/developer/cli-eligibility. Refuses with the existing subscription_expired / no_waba / account_suspended messages (same as splashify connect).
  2. Wallet balanceGET /api/v1/app/wallet/info. Prints the current balance; if it’s ≤ 0 the command refuses with a recharge prompt.

Commands gated by the preflight: broadcast create, broadcast <id> send-now, broadcast <id> restart, broadcast <id> rebroadcast. Read commands and broadcast <id> cancel are not balance-gated.

Escape hatches:

  • --yes on create / rebroadcast skips the balance soft-check.
  • SPLASHIFY_SKIP_PREFLIGHT=1 skips both checks.
  • The backend still enforces per-message via wallet_billing_service.CheckBalance — preflight is for fast, friendly UX, not security.

Read

splashify broadcasts # list splashify broadcasts --status sending --page 2 --limit 50 splashify broadcasts --search "May launch" splashify broadcast stats # global stats splashify broadcast stats --period 30d # 7d / 30d / 90d / all splashify broadcast <id> # detail splashify broadcast <id> --recompute # ask backend to recompute counts splashify broadcast <id> messages # per-message status (paginated) splashify broadcast <id> messages --status FAILED --page 1 --limit 100 splashify broadcast <id> messages --channel rcs --cumulative true splashify broadcast <id> cohorts # cohort-counts (failed/sent/delivered/read) splashify broadcast <id> export --status ALL # JSON export splashify broadcast <id> export --status FAILED --csv # CSV (binary; pipe to a file)
SubcommandEndpoint
broadcasts […]GET /api/v1/app/broadcasts
broadcast stats [--period]GET /api/v1/app/broadcasts/stats
broadcast <id> [--recompute]GET /api/v1/app/broadcasts/:id
broadcast <id> messages […]GET /api/v1/app/broadcasts/:id/messages
broadcast <id> cohortsGET /api/v1/app/broadcasts/:id/cohort-counts
broadcast <id> export […]GET /api/v1/app/broadcasts/:id/export

Write

# Create — WhatsApp template broadcast to a segment splashify broadcast create \ --name "May launch" \ --template promo_welcome \ --category MARKETING \ --language en \ --segment-ids "<seg_id_1>,<seg_id_2>" # With template variables and a header image splashify broadcast create \ --name "Welcome wave" \ --template welcome_with_image \ --category MARKETING --language en \ --params '{"components":[{"type":"body","parameters":[{"type":"text","text":"Alice"}]}]}' \ --media-url https://example.com/header.jpg \ --contact-ids "<cid_1>,<cid_2>,<cid_3>" # Scheduled splashify broadcast create \ --name "June nudge" --template june_offer --category MARKETING --language en \ --segment-ids "<seg_id>" \ --send-type scheduled --scheduled-at 2026-06-01T10:00:00Z \ --rate-limit 100 --batch-size 50 # Pure RCS splashify broadcast create \ --name "RCS launch" --template rcs_offer --category MARKETING --language en \ --segment-ids "<seg_id>" --rcs # WhatsApp with RCS fallback splashify broadcast create \ --name "WA→RCS" --template promo --category MARKETING --language en \ --segment-ids "<seg_id>" \ --rcs-fallback-template-id "<rcs_tpl_id>" --rcs-fallback-template-name rcs_promo # Cancel / restart / send-now splashify broadcast <id> cancel splashify broadcast <id> restart splashify broadcast <id> send-now # Re-send to a cohort of an existing broadcast splashify broadcast <id> rebroadcast --cohort failed splashify broadcast <id> rebroadcast --cohort delivered --name "Follow-up" \ --template promo_followup --send-at 2026-06-05T10:00:00Z
SubcommandEndpoint
broadcast create …POST /api/v1/app/broadcasts
broadcast <id> cancelPOST /api/v1/app/broadcasts/:id/cancel
broadcast <id> restartPOST /api/v1/app/broadcasts/:id/restart
broadcast <id> send-nowPOST /api/v1/app/broadcasts/:id/send-now
broadcast <id> rebroadcast --cohort …POST /api/v1/app/broadcasts/:id/rebroadcast

Required fields for create

FlagRequiredNotes
--nameyesCampaign name
--templateyesApproved template name (lower-cased before send)
--categoryyesMARKETING, UTILITY, AUTHENTICATION
--languageyesLanguage code (defaults to en)
--segment-ids or --contact-idsyesAt least one
--send-typenonow (default) or scheduled
--scheduled-atconditionalRequired when --send-type=scheduled
--paramsconditionalRequired if the template has variables/buttons (full Meta payload)
--media-urlconditionalRequired for header-media templates
--rate-limit / --batch-sizenoThrottle per second / batch size
--rcs-fallback-*noWA→RCS fallback (account must have rcs_enabled)
--rcsnoPure-RCS broadcast
--yesnoSkip the balance soft-check

What happens when preflight fails

$ splashify broadcast create --name "May launch" --template promo \ --category MARKETING --language en --segment-ids "<seg>" Wallet balance: ₹0.00 error: broadcasts are unavailable — your wallet balance is ₹0.00. Recharge: https://app.splashifypro.com/dashboard (Or run with --yes to skip this soft-check; the backend still enforces it per-message.)

For a subscription-expired account:

error: broadcasts are unavailable — your trial has ended and there is no active paid plan on this account. Upgrade your plan: https://app.splashifypro.com/settings/subscriptions Once a plan is active, retry the command.

If the backend rejects mid-send (e.g. balance drains during the broadcast), the per-message error surfaces directly:

error: HTTP 402: Insufficient balance. Required: ₹0.95, Available: ₹0.20

End-to-end recipe

# 1. Make sure you have an active subscription + non-zero balance splashify subscription | jq '.eligibility' splashify account wallet | jq '.wallet_amount' # 2. Find a segment to target SEG=$(splashify segments --search "VIP" | jq -r '.segments[0].id') # 3. Create the broadcast (preflight runs automatically) splashify broadcast create \ --name "May VIP launch" \ --template may_launch_v2 --category MARKETING --language en \ --segment-ids "$SEG" # 4. Grab the new broadcast id from the response BCAST=$(splashify broadcasts --search "May VIP launch" | jq -r '.broadcasts[0].id') # 5. Watch progress splashify broadcast "$BCAST" --recompute | jq '{status, sent, delivered, failed}' splashify broadcast "$BCAST" cohorts | jq # 6. If some failed, re-target just those splashify broadcast "$BCAST" rebroadcast --cohort failed

Common patterns

# Find every broadcast that's still sending or scheduled splashify broadcasts | jq -r '.broadcasts[] | select(.status == "sending" or .status == "scheduled") | .id' # Bulk-cancel scheduled broadcasts splashify broadcasts --status scheduled | \ jq -r '.broadcasts[].id' | \ xargs -I{} splashify broadcast {} cancel # Export the FAILED cohort as CSV splashify broadcast <id> export --status FAILED --csv > failed.csv # Total wallet spend on broadcasts over 30 days (approximate — sum of message counts × per-message rate) splashify broadcast stats --period 30d | jq '{period, sent_count, total_cost}'

Email marketing

The email command covers the full email-marketing surface — five pages and 30+ endpoints. Mirrors /email, /settings/email-domain, /email/templates, /email/audience, /email/campaigns. Available since v0.1.22.

Dashboard

splashify email # dashboard stats (default) splashify email stats # same

Backed by GET /api/v1/app/email/dashboard/stats.

Sender domains (/settings/email-domain)

Required before you can send anything — Splashify will reject campaigns whose from_email isn’t on a verified domain.

splashify email domains # list every domain + status splashify email domain example.com # one domain: DKIM/SPF/DMARC records + status splashify email domain add example.com # register a domain splashify email domain verify example.com # re-check DNS records splashify email domain delete example.com # remove
SubcommandEndpoint
domainsGET /api/v1/app/email/domains
domain <domain>GET /api/v1/app/email/domains/:domain
domain add <domain>POST /api/v1/app/email/domains
domain verify <domain>POST /api/v1/app/email/domains/:domain/verify
domain delete <domain>DELETE /api/v1/app/email/domains/:domain

Templates (/email/templates)

Templates are JSON documents produced by the visual block editor (header / image / text / button / divider / spacer / footer + variables). The CLI doesn’t model the editor — pass --file <path> to a JSON file or --data '{…}' inline.

splashify email templates # list splashify email template <id> # detail # Create — design once in the web editor, save the template_json to disk splashify email template create \ --name "Welcome" \ --subject "Welcome to Splashify Pro" \ --file ./welcome-template.json # Update — only the flags you pass change (read-modify-write) splashify email template update <id> --subject "New subject line" splashify email template update <id> --file ./welcome-template-v2.json # Render the preview HTML for a template with variables filled in splashify email template preview \ --file ./welcome-template.json \ --vars '{"first_name":"Alice","plan":"GROWTH"}' splashify email template delete <id>
SubcommandEndpoint
templatesGET /api/v1/app/email/templates
template <id>GET /api/v1/app/email/templates/:id
template create --name --subject --file/--dataPOST /api/v1/app/email/templates
template update <id> [--name --subject --file/--data]PUT /api/v1/app/email/templates/:id (read-modify-write)
template delete <id>DELETE /api/v1/app/email/templates/:id
template preview --file/--data [--vars '{…}']POST /api/v1/app/email/templates/preview

template update is read-modify-write because the backend’s PUT requires {name, subject, template_json} — sending only --subject would otherwise blank the others. Same pattern as splashify waba update.

Audience (/email/audience)

Stats

splashify email audience # stats (default) splashify email audience stats # same

GET /api/v1/app/email/audience/stats.

Contacts

splashify email audience contacts # list splashify email audience contacts --status subscribed splashify email audience contacts --search "@acme.com" splashify email audience contact <id> # detail splashify email audience contact add \ --emails "a@x.com,b@x.com,c@x.com" \ --metadata '{"plan":"pro","source":"webinar"}' \ --segments "<segment_id_1>,<segment_id_2>" splashify email audience contact update <id> --status unsubscribed splashify email audience contact update <id> --metadata '{"plan":"enterprise"}' splashify email audience contact delete <id>
SubcommandEndpoint
audience contacts [--status --search]GET /api/v1/app/email/audience/contacts
audience contact <id>GET /api/v1/app/email/audience/contacts/:id
audience contact add --emails --metadata --segmentsPOST /api/v1/app/email/audience/contacts
audience contact update <id> [--status --metadata]PUT /api/v1/app/email/audience/contacts/:id
audience contact delete <id>DELETE /api/v1/app/email/audience/contacts/:id

Segments

splashify email audience segments # list splashify email audience segment <id> # detail (list-and-filter) splashify email audience segment create --name "VIP" --description "High-value contacts" splashify email audience segment update <id> --name "VIP customers" splashify email audience segment delete <id> splashify email audience segment <id> add-contacts <cid1>,<cid2>,<cid3> splashify email audience segment <id> remove-contacts <cid1>,<cid2>
SubcommandEndpoint
audience segmentsGET /api/v1/app/email/audience/segments
audience segment <id>List-and-filter (no per-id GET)
audience segment create --name [--description]POST /api/v1/app/email/audience/segments
audience segment update <id> [--name --description]PUT /api/v1/app/email/audience/segments/:id
audience segment delete <id>DELETE /api/v1/app/email/audience/segments/:id
audience segment <id> add-contacts <ids>POST /api/v1/app/email/audience/segments/:id/contacts
audience segment <id> remove-contacts <ids>DELETE /api/v1/app/email/audience/segments/:id/contacts

Campaigns (/email/campaigns)

splashify email campaigns # list splashify email campaign <id> # detail (status, stats, recipients) # Create — recipients are either segments OR specific contact IDs (or both) splashify email campaign create \ --name "May launch" \ --template-id <template_id> \ --from-name "Acme" \ --from-email "hi@yourdomain.com" \ --reply-to "support@yourdomain.com" \ --segment-ids <segment_id_1>,<segment_id_2> \ --scheduled-at 2026-06-01T10:00:00Z # Or target specific contacts directly splashify email campaign create \ --name "Win-back" --template-id <id> \ --from-name "Acme" --from-email "hi@yourdomain.com" \ --contact-ids <cid1>,<cid2> # Send or cancel splashify email campaign send <id> # send now (or trigger scheduled send) splashify email campaign cancel <id> # cancel a scheduled / in-progress campaign
SubcommandEndpoint
campaignsGET /api/v1/app/email/campaigns
campaign <id>GET /api/v1/app/email/campaigns/:id
campaign create --name --template-id --from-name --from-email …POST /api/v1/app/email/campaigns
campaign send <id>POST /api/v1/app/email/campaigns/:id/send
campaign cancel <id>POST /api/v1/app/email/campaigns/:id/cancel

--from-email must be on a verified domain (splashify email domainsstatus: verified). If you skip --scheduled-at the campaign is created in draft and sent only when you run campaign send.

End-to-end recipe — design a campaign and ship it

# 1. Verify your sender domain splashify email domain add example.com # (paste the returned DKIM/SPF records into your DNS, wait a minute) splashify email domain verify example.com # 2. Build the template in the web app once, then export it splashify email template <existing_id> | jq '.template.template_json' > /tmp/welcome.json splashify email template create --name "May welcome" --subject "Welcome 👋" --file /tmp/welcome.json # 3. Build the audience splashify email audience segment create --name "May launch" SEG=$(splashify email audience segments | jq -r '.segments[] | select(.name == "May launch") | .segment_id') splashify email audience contact add --emails "a@x.com,b@x.com" --segments "$SEG" # 4. Create the campaign and send TPL=$(splashify email templates | jq -r '.templates[] | select(.name == "May welcome") | .template_id') splashify email campaign create \ --name "May welcome" --template-id "$TPL" \ --from-name "Acme" --from-email "hi@example.com" \ --segment-ids "$SEG" CAMP=$(splashify email campaigns | jq -r '.campaigns[] | select(.name == "May welcome") | .campaign_id') splashify email campaign send "$CAMP" # 5. Track delivery splashify email campaign "$CAMP" | jq '{status, sent_count, delivered_count, opened_count, clicked_count}'

Common patterns

# Pending-verification domains splashify email domains | jq '.domains[] | select(.status != "verified")' # Bulk-import contacts from a CSV column (one email per line) xargs -I{} splashify email audience contact add --emails {} < emails.txt # Pause everything in flight splashify email campaigns | \ jq -r '.campaigns[] | select(.status == "sending" or .status == "scheduled") | .campaign_id' | \ xargs -I{} splashify email campaign cancel {} # Find templates that haven't been used in any campaign USED=$(splashify email campaigns | jq -r '.campaigns[].template_id' | sort -u) splashify email templates | jq --argjson used "$(echo "$USED" | jq -R . | jq -s .)" \ '.templates[] | select(.template_id as $id | $used | index($id) | not) | {template_id, name}'

AI / Voice AI credits (read-only)

The credits command mirrors the credit widgets on /dashboard — your general AI credit balance plus the Voice AI widget’s full state (per-minute rate, trial minutes remaining, available minutes, and the list of voice agents). Available since v0.1.21.

Read-only by design. Recharges, payment verification, and Razorpay flows stay in the web app. The CLI shows what’s currently on the account.

splashify credits # consolidated view (all sections) splashify credits ai # AI credit balance + info splashify credits transactions # AI credit transaction history splashify credits voice # voice AI rate + balance + trial splashify credits agents # voice AI agents # Aliases splashify credit # singular splashify ai-credits # full word
SubcommandBacked by
(none — default)Merges all three reads below into one JSON object
ai / infoGET /api/v1/app/ai-credits/info
transactions / txGET /api/v1/app/ai-credits/transactions
voice / voice-aiGET /api/v1/app/voice-ai/rate
agentsGET /api/v1/app/voice-ai/agents

Voice AI rate response

{ "rate_per_minute": 0.50, "trial_credited": true, "trial_minutes_remaining": 47, "ai_credit_balance": 123.45, "available_minutes": 246 }

available_minutes = ai_credit_balance ÷ rate_per_minute + any remaining trial_minutes_remaining. Use it to gauge when the next recharge will be needed.

Consolidated view shape

{ "ai_credit_info": { ... balance, lifetime spent, last recharge, ... }, "voice_ai_rate": { "rate_per_minute": ..., "ai_credit_balance": ..., ... }, "voice_ai_agents": { "agents": [ ... ] } }

A section that fails for any reason is reported as {"error": "..."} for that key — the rest of the view still renders.

Common patterns

# Just the headline numbers splashify credits | jq '{ ai_balance: .ai_credit_info.balance, voice_balance: .voice_ai_rate.ai_credit_balance, voice_rate: .voice_ai_rate.rate_per_minute, available_minutes: .voice_ai_rate.available_minutes, trial_minutes: .voice_ai_rate.trial_minutes_remaining, agent_count: (.voice_ai_agents.agents | length) }' # Recent AI credit transactions splashify credits transactions | jq '.transactions[] | {at: .created_at, amount, type, note}' # Voice AI low-credit alarm (alert if < 30 minutes of runway) MINUTES=$(splashify credits voice | jq '.available_minutes') if [ "$MINUTES" -lt 30 ]; then echo "WARN: only $MINUTES voice-AI minutes left — recharge in app.splashifypro.com" fi # Which voice agents are active splashify credits agents | jq -r '.agents[] | select(.is_active) | "\(.name)\t\(.status)"'

Activity logs (owner-only, read-only)

The activity command mirrors /activity-logs — the audit trail of every user and team action across your account (logins, contact edits, template changes, conversation events, tag mutations, and more). Owner-only on the backend; non-owner oc_live_ tokens get a 403 here. Available since v0.1.20.

splashify activity # latest 100 logs splashify activity --limit 200 # custom limit splashify activity --action login # filter by action splashify activity --entity contact # filter by entity type splashify activity --entity contact --entity-id <id> splashify activity --actor <user_id> # filter by actor splashify activity --search "john" # client-side text search # Aliases splashify activity-logs # full word splashify logs # short alias
FlagSent to backendNotes
--action <name>action=<name>login, logout, created, updated, deleted, assigned, unassigned, blocked, unblocked, resolved, reopened
--entity <type>entity_type=<type>auth, contact, conversation, template, message, tag
--entity-id <id>entity_id=<id>Narrow to one specific entity
--actor <user_id>actor_id=<user_id>Narrow to one specific user
--limit <N>limit=<N>Server default if omitted (page uses 100)
--search "<q>"(none — client-side)Substring match across actor_name, actor_email, description, entity_name. Mirrors the page’s search box.

Output shape

{ "success": true, "logs": [ { "log_id": "uuid", "action": "created", "entity_type": "contact", "entity_id": "uuid", "entity_name": "Acme Corp", "actor_id": "uuid", "actor_name": "John Roe", "actor_email": "john@acme.com", "actor_role": "owner", "description": "John Roe created contact Acme Corp", "metadata": "{...}", "created_at": "2026-05-19T08:05:33.163Z" } ] }

For entity_type: "auth" events, metadata is a JSON string with ip, city, region, country, org, timezone — pipe through jq to expand it.

Common patterns

# Just the most recent 20 logins, with location info parsed splashify activity --action login --limit 20 | \ jq '.logs[] | {at: .created_at, who: .actor_email, where: (.metadata | fromjson | "\(.city), \(.country)")}' # Every action one user took in the last batch splashify activity --actor <user_id> --limit 200 | \ jq -r '.logs[] | "\(.created_at)\t\(.action)\t\(.entity_type)\t\(.entity_name)"' # All deletions across the account splashify activity --action deleted --limit 200 | jq # Recent logins from outside India splashify activity --action login --limit 100 | \ jq '.logs[] | select(.metadata | length > 0) | .metadata | fromjson | select(.country != "IN")' # Quick text search (mirrors the page's search box) splashify activity --search "stop" | jq '.count, .logs[].description'

Attribute commands

The attribute / attributes commands mirror Settings → Attributes — custom columns added to each contact (Company, Job Title, Birthday, etc.). Full CRUD plus the page’s visibility toggle and up/down reorder actions. Available since v0.1.12.

Read

splashify attributes # list every attribute splashify attributes --search co # client-side substring filter on label splashify attribute <id> # show one (list-and-filter)

Write

# Create a text attribute splashify attribute create --label "Company" --type TEXT # Create a required dropdown with options splashify attribute create \ --label "Lead Source" \ --type SELECT \ --options "Web,Referral,Ads,Trade Show" \ --required true \ --help "Where this lead came from" # Multi-select splashify attribute create --label "Interests" --type MULTISELECT \ --options "Sales,Marketing,Support" # Update — only flags you pass are changed (read-modify-write under the hood) splashify attribute update <id> --label "Company Name" splashify attribute update <id> --required false --help "Optional from now on" splashify attribute update <id> --options "Web,Referral,Ads,Trade Show,Other" # Toggle visibility on the contacts table splashify attribute toggle-visibility <id> # Move up or down in display order splashify attribute reorder <id> up splashify attribute reorder <id> down # Delete (soft-delete; existing data on contacts is preserved per the page) splashify attribute delete <id>

Endpoint reference

SubcommandEndpoint
attributes [--search …]GET /api/v1/app/attributes (filter client-side)
attribute <id>List-and-filter (backend has no per-id GET)
attribute create …POST /api/v1/app/attributes
attribute update <id> … (alias edit)PUT /api/v1/app/attributes/:id
attribute delete <id> (aliases rm, remove)DELETE /api/v1/app/attributes/:id
attribute toggle-visibility <id> (alias toggle)POST /api/v1/app/attributes/:id/toggle-visibility
attribute reorder <id> up|down (alias move)POST /api/v1/app/attributes/reorder

Attribute types

TypeAccepts on the contactNotes
TEXTany stringDefault; labels with spaces become underscores
NUMBERnumeric
EMAILRFC-5322 email
PHONEE.164 phone
DATEISO 8601 date
SELECTone of --options--options "a,b,c" required
MULTISELECTsubset of --options--options "a,b,c" required
URLURL
CHECKBOXbool

Why update is read-modify-write

The backend’s PUT /api/v1/app/attributes/:id requires label + type to be present and writes every optional column (is_visible, is_required, options, default_value, help_text) on every call. The CLI loads the current row first, overlays only the flags you passed, then submits the full body so omitted fields stay intact. Same pattern as splashify waba update and splashify opt ….

Common patterns

# IDs of all required attributes splashify attributes | jq -r '.attributes[] | select(.is_required) | .id' # Look up an attribute id by label splashify attributes | jq -r '.attributes[] | select(.label == "Company") | .attribute_id' # Hide every SELECT attribute from the contacts table (visibility toggle) splashify attributes | jq -r '.attributes[] | select(.type == "SELECT" and .is_visible) | .attribute_id' | \ xargs -I{} splashify attribute toggle-visibility {} # Bulk delete attributes whose label starts with "test_" splashify attributes | jq -r '.attributes[] | select(.label | startswith("test_")) | .attribute_id' | \ xargs -I{} splashify attribute delete {}

Segment commands

segments / segment mirror Settings → Segments — the saved audience filters used to slice your contact list for broadcasts and reporting. Full CRUD plus per-segment introspection. Available since v0.1.11.

Read

splashify segments # list, first page splashify segments --page 2 --limit 50 # paginate splashify segments --search vip # server-side name search splashify segments stats # overall stats splashify segment <id> # one segment splashify segment <id> contacts # contacts in this segment splashify segment <id> contacts --page 2 --limit 100 splashify segment <id> count # current member count splashify segment <id> refresh # recompute count (for dynamic segments)
SubcommandEndpoint
segments [--search --page --limit]GET /api/v1/app/segments
segments statsGET /api/v1/app/segments/stats
segment <id>GET /api/v1/app/segments/:id
segment <id> contacts [--page --limit]GET /api/v1/app/segments/:id/contacts
segment <id> countGET /api/v1/app/segments/:id/count
segment <id> refresh / recomputePOST /api/v1/app/segments/:id/refresh

Write

# Create — --filters JSON is required (see DSL below) splashify segment create \ --name "VIP iOS users" \ --description "Tagged VIP, on iOS, marketing-opted-in" \ --filters '{ "conditions": [ {"field": "tags", "operator": "includes", "value": "VIP"}, {"field": "hasOptedOut", "operator": "equals", "value": false} ], "logic": "and" }' \ --dynamic true \ --active true # Update — every flag is optional; only what you pass is sent (PATCH semantics) splashify segment update <id> --name "VIP — refreshed" splashify segment update <id> --active false splashify segment update <id> --filters '{"conditions":[…],"logic":"or"}' # Delete splashify segment delete <id>
SubcommandEndpoint
segment create --name --filters …POST /api/v1/app/segments
segment update <id> [flags…] (aliases: edit)PATCH /api/v1/app/segments/:id
segment delete <id> (aliases: rm, remove)DELETE /api/v1/app/segments/:id

Filter DSL reference

The --filters JSON is shipped to the backend verbatim. Shape:

{ "conditions": [ {"field": "<field>", "operator": "<operator>", "value": "<value>"}, {"field": "<field>", "operator": "<operator>"} // value omitted for isEmpty / isNotEmpty ], "logic": "and" // or "or" }

Fields (from CreateSegmentDialog):

FieldTypeNotes
displayNametextContact’s display name
phoneNumbertextE.164 phone, e.g. +91…
emailtext
tagsarray of stringsUse includes / notIncludes
hasOptedOutboolMarketing opt-in flag
isBlockedbool
createdAttimestampISO 8601
updatedAttimestampISO 8601

Operators:

OperatorWorks on
equals, notEqualstext, bool
contains, notContainstext
startsWith, endsWithtext
isEmpty, isNotEmptytext (value omitted)
includes, notIncludesarray (tags)

The web app (/settings/segments) is a visual builder for this DSL. Use it once to design a complex segment, then splashify segment <id> to see the JSON it produced, then build similar segments programmatically.

Common jq patterns

# IDs of all active segments splashify segments --limit 100 | jq -r '.segments[] | select(.is_active) | .id' # Get a segment ID by exact name splashify segments --search "VIP" | \ jq -r '.segments[] | select(.name == "VIP") | .id' # Use a segment ID in a broadcast (existing command) SEGMENT_ID=$(splashify segments --search "VIP" | jq -r '.segments[0].id') splashify broadcast create --name "May launch" \ --template payment_failed \ --audience-type segment --audience-id "$SEGMENT_ID" # Trigger refresh on every dynamic segment (e.g. nightly cron) splashify segments --limit 100 | \ jq -r '.segments[] | select(.is_dynamic) | .id' | \ xargs -I{} splashify segment {} refresh

Tag CRUD commands

The tag / tags commands mirror Settings → Tags — full CRUD on the tag library used to organise contacts. Available since v0.1.10.

splashify tags # list every tag splashify tags --search vip # client-side substring filter splashify tag create "VIP" # create a tag splashify tag create High Value Customer # multi-word names work without quotes splashify tag rename <id> "Important" # rename a tag splashify tag delete <id> # delete (unmaps from every contact)
CommandEndpoint
tags [--search ...]GET /api/v1/app/tags (filter is client-side)
tag create "<name>"POST /api/v1/app/tags {"name":"…"}
tag rename <id> "<name>" / tag update … / tag edit …PUT /api/v1/app/tags/:id {"name":"…"}
tag delete <id> / tag rm <id>DELETE /api/v1/app/tags/:id

Output shape

{ "success": true, "tags": [ {"id": "uuid", "name": "VIP"}, {"id": "uuid", "name": "Newsletter"} ], "count": 2 }

Naming convention

The CLI uses plural for listing and singular for actions — the same pattern as contacts / contact, broadcasts / broadcast. There is no collision with contact tag <id> --tags … (that’s the existing “tag a specific contact” command); the new tag group operates on tag entities themselves.

Deleting a tag is destructive

splashify tag delete <id> unmaps the tag from every contact that had it. The web app warns about this; the CLI does not prompt (it would break automation) — pipe through xargs if you want to script it, but double-check the IDs first:

# Find which tags would be affected splashify tags | jq -r '.tags[] | select(.name | test("^test-")) | "\(.id)\t\(.name)"' # Bulk delete tags whose name starts with "test-" — irreversible splashify tags | jq -r '.tags[] | select(.name | test("^test-")) | .id' | \ xargs -I{} splashify tag delete {}

Common patterns

# Count tags splashify tags | jq '.tags | length' # Get a tag's id by exact name splashify tags | jq -r '.tags[] | select(.name == "VIP") | .id' # Rename the "VIP" tag to "Top Customer" ID=$(splashify tags | jq -r '.tags[] | select(.name == "VIP") | .id') splashify tag rename "$ID" "Top Customer" # Tag a specific contact with an existing tag (note: contact tag, not tag) splashify contact tag <contact_id> --tags VIP,lead

Opt-out / Opt-in keyword commands

The opt command mirrors Settings → OPT Management — the keyword lists that automatically opt a contact out of (or back into) messaging, plus the optional auto-response message for each. Available since v0.1.9.

splashify opt # full settings (default) splashify opt out # show just opt_out splashify opt in # show just opt_in splashify opt out add STOP UNSUBSCRIBE QUIT # add keywords (case-insensitive dedupe) splashify opt out remove QUIT # remove keywords splashify opt out response "You have been opted out of all marketing messages." splashify opt out response-on # enable the auto-response splashify opt out response-off # disable the auto-response splashify opt in add START SUBSCRIBE splashify opt in remove START splashify opt in response "Welcome back — you'll start receiving updates again." splashify opt in response-on | response-off
SubcommandWhat it does
(none — default) / status / showGET /app/opt-settings — both sides
out / inPrint only that section
out add <kw1> [<kw2> ...]Append keywords to opt_out.keywords; existing entries (case-insensitive) are skipped
out remove <kw1> [<kw2> ...]Drop matching keywords (case-insensitive)
out response "<text>"Set opt_out.response ("" to clear)
out response-on / response-offToggle opt_out.response_enabled
in ...Same actions, applied to opt_in

Why writes do read-modify-write

PUT /api/v1/app/opt-settings requires both opt_out and opt_in in the body — sending just one side would clear the other. The CLI always loads the current settings first, mutates the requested side, and submits the merged payload. Same pattern as splashify waba update.

What the response shape looks like

{ "success": true, "opt_out": { "keywords": ["STOP", "UNSUBSCRIBE"], "response": "You have been opted out.", "response_enabled": true }, "opt_in": { "keywords": ["START", "SUBSCRIBE"], "response": "Welcome back.", "response_enabled": false } }

jq snippets

# Just the opt-out keywords splashify opt | jq -r '.opt_out.keywords[]' # Quick check: is auto-response enabled on either side? splashify opt | jq '{out: .opt_out.response_enabled, in: .opt_in.response_enabled}' # Migrate keywords from one side to the other (out → in) splashify opt | jq -r '.opt_out.keywords[]' | \ xargs -r splashify opt in add

Media library commands

The media command surfaces the /media page — list every file uploaded to your account, see storage usage, upload new files from disk, and delete files by media_id. Available since v0.1.8.

splashify media # list every file (URLs + details) splashify media list # same as above (alias) splashify media list --type image # filter by type splashify media storage # storage quota + usage splashify media upload ./logo.png # upload a file from disk splashify media upload /var/files/invoice.pdf splashify media delete 7a32bce6-910d-4b1f-... # remove a file by media_id
SubcommandBacked by
(none — default) / list / lsGET /api/v1/app/media[?type=...]
storage / quotaGET /api/v1/app/media/storage
upload <path>POST /api/v1/app/media/upload (multipart, field file)
delete <media_id> / rm <media_id>DELETE /api/v1/app/media/:media_id

What each file row contains

{ "media_id": "uuid", "file_name": "media/.../1700000000-original.png", "original_name": "logo.png", "file_url": "https://splashify.blr1.cdn.digitaloceanspaces.com/media/...", "file_type": "image", "mime_type": "image/png", "file_size": 12345, "file_extension": ".png", "created_at": "2026-05-19T13:57:23.582Z" }

The file_url is the public CDN URL of the file. You can hand it to splashify message media --url <file_url> to send the file in a WhatsApp message without re-uploading.

Accepted file types

Enforced by the backend’s classifyFileType:

TypeExtensionsMax size
image.jpg .jpeg .pngper backend config
video.mp4per backend config
audio.mp3 .ogg .webm .aac .amr .m4aper backend config
document.pdf .xlsx .xls .doc .docx .csvper backend config

Other extensions are refused with a 400 listing the accepted set. The exact per-type size caps live in the backend and are reported in the error message when a file is too large.

Storage quota

splashify media storage returns the current usage and limit:

splashify media storage | jq # { # "success": true, # "storage_used": 524288, # "storage_limit": 5368709120, # "files_count": 12, # ... # }

When an upload would exceed the quota the backend refuses with HTTP 400 and the JSON includes storage_used and storage_limit so the CLI surfaces a clear “storage quota exceeded” message.

Common jq one-liners

# Just the URLs of every image splashify media list --type image | jq -r '.files[].file_url' # Total bytes used by uploaded videos splashify media list --type video | jq '[.files[].file_size] | add' # Most recent upload of any type splashify media | jq '.files | sort_by(.created_at) | reverse | .[0]' # Bulk delete every audio file (careful — irreversible) splashify media list --type audio | jq -r '.files[].media_id' | \ xargs -I{} splashify media delete {}

Subscription commands (read-only)

The subscription command mirrors Settings → Subscriptions — the active plan, add-ons, and the list of plans available for upgrade. Available since v0.1.6.

Read-only by design. No upgrade, no payment, no coupon validation. To change a plan, use the web app.

splashify subscription # consolidated view splashify subscription status # current plan + add-ons splashify subscription plans # available plans you can upgrade to splashify subscription addons # add-ons on the current plan
SubcommandBacked by
(none — default)Merges three reads: /app/plans/subscription, /app/developer/cli-eligibility, /plans
status / currentGET /api/v1/app/plans/subscription
plans / availableGET /api/v1/plans
addonsProjection of addons[] from /app/plans/subscription

Subscription gate at connect

splashify connect calls /api/v1/app/developer/cli-eligibility after the token is validated. The endpoint now refuses with reason: "subscription_expired" when:

  • The user has no active paid plan (plan_status != "active" or the plan has expired), AND
  • The trial has ended (trial_ends_at is in the past or missing).

In that case the CLI prints:

your trial has ended and there is no active paid plan on this account. The splashify CLI requires an active subscription (paid plan OR unexpired trial). Upgrade your plan: https://app.splashifypro.com/settings/subscriptions Run "splashify subscription" once a plan is active to confirm, then re-run "splashify connect".

Free-trial users continue to work normally — the gate only fires when both trial and paid plan are unavailable.

Per-feature upgrade prompts

If a backend endpoint refuses a CLI command because the user’s plan is below the tier required for that feature, the response should be:

{ "success": false, "error": "plan_required", "required_plan": "GROWTH", "current_plan": "STARTER", "feature": "broadcasts.create", "upgrade_url": "https://app.splashifypro.com/settings/subscriptions" }

The CLI auto-detects this shape on any HTTP 402/403 and prints:

error: this feature requires a higher plan (you are on STARTER, needed: GROWTH) Feature: broadcasts.create Upgrade your plan: https://app.splashifypro.com/settings/subscriptions

The CLI never invents tier rules — the backend stays the source of truth. Adding a new tier gate only requires the backend to return this shape; the CLI displays it correctly without a new release.

Billing commands (read-only)

The billing command mirrors everything the Settings → Billing page reads — the GST billing profile, the active subscription, recent invoices, and billing logs. Available since v0.1.5.

Read-only by design. No profile update, no GSTIN validation, no certificate upload, no payment initiation. To change billing details, use the web app.

splashify billing # consolidated view (all sections) splashify billing profile # GST profile + billing address splashify billing invoices # invoice list splashify billing logs # billing log entries (all-time) splashify billing logs --period 30d # narrow the window splashify billing logs --limit 100 # cap the result count
SubcommandBacked by
(none — default)Merges all five reads below into one JSON object
profileGET /api/v1/app/billing
invoicesGET /api/v1/app/invoices
logs [--period --limit]GET /api/v1/app/expenses/billing-logs

The default consolidated view returns:

{ "profile": { "billing": { "display_name": "...", "gst_no": "...", "billing_address": { ... } } }, "subscription": { "user": { "plan_name": "...", "plan_status": "...", "plan_expires_at": "...", ... } }, "wallet": { "wallet_amount": 1234.56, ... }, "invoices": { "invoices": [ ... ] }, "logs": { ... } }

A section that fails for any reason is reported as {"error": "..."} for that key — the rest of the view still renders.

Common one-liners with jq:

# Just the billing-critical fields splashify billing | jq '{ legal_name: .profile.billing.display_name, gst_no: .profile.billing.gst_no, gst_treatment: .profile.billing.gst_treatment, city: .profile.billing.billing_address.city, state: .profile.billing.billing_address.state, plan: .subscription.user.plan_name, expires: .subscription.user.plan_expires_at, wallet: .wallet.wallet_amount, invoice_count: (.invoices.invoices | length) }' # Most recent invoice splashify billing invoices | jq '.invoices[0]' # All invoice numbers and totals splashify billing invoices | jq -r '.invoices[] | "\(.invoice_number)\t\(.total)\t\(.status)"' # Billing logs for the last 30 days splashify billing logs --period 30d | jq

Downloading an invoice PDF is not exposed as a friendly command — the backend returns a binary response. Use the URL pattern https://api.splashifypro.com/api/v1/app/invoices/<invoice_id>/download with your oc_live_ token in the Authorization header (curl works), or open the invoice from the web app.

WhatsApp Business Account (WABA) commands

The waba command mirrors everything the Dashboard page in the app shows about your connected WhatsApp Business Account — and lets you update the business profile, refresh data from Meta, register the phone, and manage the Official Business Account (OBA) status. Available since v0.1.3.

splashify waba # show the full WABA details splashify waba | jq '{phone_number, waba_id, quality_rating, messaging_limit_tier}' splashify waba setup-status # high-level setup checklist splashify waba sync # pull fresh data from Meta splashify waba register-phone # (re-)register the phone with Meta splashify waba oba-status # Official Business Account status splashify waba oba-apply # apply for Official Business Account splashify waba request-deletion # request WABA deletion (destructive)

Update the business profile

# Single field — other fields are preserved splashify waba update --about "Premium WhatsApp messaging for businesses" # Multiple fields at once splashify waba update \ --about "Premium WhatsApp messaging" \ --description "Splashify Pro — broadcasts, templates, AI-driven flows" \ --email "hello@splashifypro.com" \ --address "Kolkata, West Bengal, India" \ --vertical "PROF_SERVICES" \ --websites "https://splashifypro.com,https://app.splashifypro.com"

Partial updates are safe. Since v0.1.7 the CLI does a read-modify-write — it fetches the current profile, overlays whichever flags you passed, and submits the merged body. Fields you didn’t mention stay untouched.

Earlier CLI versions sent a partial body that the backend blanked the omitted columns from. The backend was also patched in the same release so any client (CLI, MCP, your own scripts) can now send a partial body safely; if you are on a CLI older than v0.1.7 and the backend deploy is pending, pass every field you want to keep.

FlagWhat it sets
--aboutShort bio (Meta limit ~139 chars)
--descriptionLonger business description
--addressPhysical address
--emailContact email
--verticalBusiness category. Meta enum: AUTO, BEAUTY, APPAREL, EDU, ENTERTAIN, EVENT_PLAN, FINANCE, GROCERY, GOVT, HOTEL, HEALTH, NONPROFIT, PROF_SERVICES, RETAIL, TRAVEL, RESTAURANT, NOT_A_BIZ, OTHER
--websitesComma-separated URLs (Meta allows up to 2)
--dataRaw JSON merged on top of the flags — for advanced fields like profile_picture_handle

Fields returned by splashify waba

Backed by GET /api/v1/app/dashboard/whatsapp-status. The response includes:

GroupFields
Identifierswaba_id, phone_number_id, phone_number, business_id, display_name
Verificationverified_name, code_verification_status, name_status, business_verification_status, is_official_business_account
Quality & limitsquality_rating, messaging_limit_tier, throughput_level
Statewaba_status, phone_status, platform_type, is_on_biz_app, last_synced_at
Business profileprofile_about, profile_description, profile_address, profile_email, profile_vertical, profile_websites, profile_picture_url
Accountwaba_name, waba_currency

If the response includes "needs_sync": true, run splashify waba sync; the next call will return the full set.

AI Agents commands

ai-agents (plural) and ai-agent (singular) mirror /ai-agents and the per-agent detail page — full agent CRUD, default-agent toggle, and knowledge-base management.

splashify ai-agents # list splashify ai-agent <agent_id> # show one splashify ai-agent create --name "Bot" --agent-type support \ [--channel whatsapp|instagram] [--industry …] \ [--use-case …] [--role …] [--goal …] [--instructions …] splashify ai-agent update <id> [--name] [--role] [--goal] [--instructions] \ [--status] [--tone] [--ai-provider] [--ai-model] \ [--temperature 0.7] [--processing-msg true|false] \ [--processing-msg-text …] [--image-recognition true|false] splashify ai-agent set-default <id> splashify ai-agent unset-default <id> splashify ai-agent delete <id> splashify ai-agent knowledge <id> # list knowledge files splashify ai-agent knowledge <id> upload <file> # .pdf / .docx / .md / .txt (max 15MB) splashify ai-agent knowledge <id> delete <file_id>

Full guide: AI Agents — covers channel gates (WhatsApp vs Instagram), the sparse-PUT update semantics, knowledge base management, and end-to-end workflows.

Integrations commands

integrations mirrors /integrations — per-slug behaviour configs, OAuth account connections, and webhook event logs.

splashify integrations # configs (default view) splashify integrations config <slug> # show one splashify integrations config <slug> save \ --enabled true --template <id> [--config '{…}'] [--vars '{…}'] \ [--phone-field …] [--events '{…}'] splashify integrations config <slug> delete splashify integrations accounts # OAuth connections splashify integrations account <id> disconnect splashify integrations token # mint a connect-token splashify integrations logs [--limit N] [--slug …] # webhook event feed splashify integrations log <log_id> # one event

Full guide: Integrations — covers per-slug configs, the --events per-event automation map, OAuth account lifecycle, and webhook-log debugging.

IP allowlist commands (paid)

allowed-ips (aliases: ip-allowlist, ips) mirrors /settings/allowed-ips. Paid feature; trial users get a friendly feature_locked upgrade prompt.

splashify allowed-ips # list splashify allowed-ips add --name "Office" --mode single --ip 203.0.113.4 splashify allowed-ips add --name "VPN" --mode range \ --start 10.0.0.0 --end 10.0.0.255 splashify allowed-ips delete <entry_id>

Full guide: IP allowlist — covers behaviour when zero vs one+ entries are present, the safety-net rule that lets you always reach the allowlist endpoints, and limits (50 entries, IPv4 only).

Click-to-WhatsApp Ads commands

ctwa (alias: meta-ads) mirrors /settings/ctwa — Meta OAuth code exchange, Conversion API (CAPI) dataset + event triggers, test-event firing, and ads inventory.

splashify ctwa # CAPI status (default) splashify ctwa capi # show CAPI config splashify ctwa capi save [--dataset-id] [--lead-enabled] [--lead-trigger] \ [--lead-tag] [--purchase-enabled] [--purchase-trigger] \ [--purchase-tag] [--purchase-currency] [--purchase-value] splashify ctwa capi send-event --event-type lead|purchase --phone +91… \ [--value 99.99] [--currency INR] splashify ctwa exchange-code --code <code> [--granted-scopes '[…]'] [--redirect-uri …] splashify ctwa refresh-token splashify ctwa ads # list ads (resolves user_id from /app/me) splashify ctwa ad <ad_id>

Full guide: Click-to-WhatsApp Ads — covers the graphical OAuth handshake, the CAPI lead/purchase event triggers, the auto-create-dataset behaviour, and the per-ad inventory commands.

Template commands (WhatsApp + RCS)

The templates / template commands mirror /templates and /templates/create for WhatsApp; the rcs templates / rcs template commands mirror /templates/rcs/create for RCS.

# WhatsApp splashify templates # list splashify template <template_id> # show one splashify templates sync # sync ALL from Meta splashify templates sync <template_id> # sync one splashify templates upload-media <file> # get a Meta media handle splashify templates create --name "promo" --language en --category MARKETING \ --text "Welcome to our store!" splashify templates create --file ./template.json # full payload from disk splashify templates delete <template_id> [--name <name>] # RCS splashify rcs templates # list splashify rcs template <template_id> # show one splashify rcs template <template_id> check-status # poll approval splashify rcs templates upload-media <file> [--height SHORT|MEDIUM|TALL] splashify rcs templates create --name "rcs_promo" --type basic --text "Hi!" splashify rcs templates create --name "rcs_card" --type rich_card --data '{…}' splashify rcs templates create --name "rcs_carousel" --type carousel --file ./c.json splashify rcs templates delete <template_id>

Full guide: see the Templates page for component grammar (HEADER/BODY/FOOTER/BUTTONS/CAROUSEL/LIMITED_TIME_OFFER), media handle two-step flow, RCS RML template_data shapes (basic / rich_card / carousel) with all five suggestion types (message, url, dial, calendar, location), bulk-sync cron, submit-and-wait scripts, and troubleshooting.

WhatsApp Flows commands

The flows and flow commands mirror the /flows page — list your Meta-managed WhatsApp Flow forms, sync them from Meta, view per-flow submissions, and deprecate old flows. The “Sync from Meta” and “Create in Meta” buttons on the page are exposed as CLI subcommands.

splashify flows # list flows splashify flows sync # pull fresh data from Meta splashify flows create-url # URL to open Meta Flow Builder splashify flow <flow_id> # show one flow splashify flow <flow_id> responses --limit 50 # list submissions splashify flow <flow_id> deprecate # retire a flow splashify flow response <response_id> # show one submission by id

Flow creation is graphical-only. Meta only exposes the Flow JSON editor through its in-browser Flow Builder. flows create-url returns the deep-link URL; open it in a browser, publish there, then flows sync to pull the new flow into your account.

Full guide: see the WhatsApp Flows page for the data lifecycle, the response shape, advanced workflows (nightly cron sync, paginated dump to CSV, deprecate-by-age cleanup), and troubleshooting.

Devices / sessions commands

The devices command (aliases: sessions, ses) and session (alias: device) mirror the Settings → Devices page — list every device signed into your Splashify Pro account and remotely log any of them out.

splashify devices # list every active session splashify devices list --platform web # filter by platform splashify devices list --ip 203.0.113.4 # filter by IP splashify session <session_id> # show one session splashify devices logout <session_id> # revoke one session splashify devices logout-all # revoke every session (prompts) splashify devices logout-all --yes # skip the prompt (scripts) splashify devices logout-all --platform web # only web sessions

The CLI keeps working after logout-all — access tokens (oc_live_…) are stored in a separate table from login sessions.

Full guide: see the Devices & Sessions page for the response shape, the is_current nuance, security notes, and end-to-end workflows (revoke by age, by IP, by platform; keep just the latest session; etc.).

Instagram automation commands

The instagram command (alias: ig) mirrors the /instagram-automation page — connect your Instagram Login account, manage comment-to-DM automation rules, send DMs in existing conversations, check Meta’s reply window, sync historical chats, and read the activity log.

# Setup splashify instagram oauth-url # → open authorize_url in a browser splashify instagram connect --code <code> splashify instagram # show connected account splashify instagram disconnect # Discovery splashify instagram media --limit 25 # your IG posts (rule targets) splashify instagram logs --limit 50 # automation activity feed splashify instagram sync # backfill conversations from Meta # Comment-to-DM rules (CRUD + toggle) splashify instagram rules # list splashify instagram rule <id> # show one splashify instagram rule create --media-id <id> \ --keyword "buy" --dm-message "Here's the link…" \ --comment-reply "DM sent 💌" --active true splashify instagram rule update <id> --keyword "new" # RMW + PATCH splashify instagram rule toggle <id> # flip is_active splashify instagram rule delete <id> # Send a DM (gated by Meta's 24h/7d window) splashify instagram window --conversation <conversation_id> splashify instagram dm --conversation <id> --message "Hey!" \ [--media-url https://… --media-type image|video|audio]

Full guide: see the Instagram Automation page for the full OAuth flow, every endpoint, the reply-window rules, the rule shape + field reference, advanced workflows (CSV-driven bulk-create, mass-toggle by media, body swaps), and troubleshooting.

Canned message commands

The canned command (aliases: cm, cms, canned-messages) mirrors the Settings → Canned Messages page — full CRUD plus the activate/deactivate toggle on saved WhatsApp replies. Supports text, image, video, audio, document, and any advanced Meta interactive payload.

splashify canned # list every canned message splashify canned list --type IMAGE # filter by message_type splashify canned <message_id> # show one splashify canned create --name "Welcome" --type TEXT --text "Hi there!" splashify canned create --name "Receipt" --type IMAGE \ --url https://cdn.example.com/receipt.png --caption "Your receipt" splashify canned update <id> --text "Updated body" splashify canned toggle <id> # flip is_active splashify canned delete <id> # permanent removal

Full guide: see the Canned Messages page for every message type, the Meta-Cloud-API payload shapes, advanced --payload examples (interactive buttons / list / CTA / location / contact / address), and bulk workflows.

Team / Agent commands

The team (aliases: agents, members) and member (alias: agent) commands mirror the Settings → Agents page — invite a new team member, update their role + per-page permissions, resend OTPs/invites, and remove them. The CLI wraps the two-step backend flow (POST /app/team followed by POST /app/team/verify-otps) into a single team add command that prompts for the OTP after creating the member.

splashify team # list members + limit + count splashify member <id> # show one member # Invite an agent with full access — prompts for the OTP that lands on # the OWNER's email + WhatsApp splashify team add --name "Alice" --email alice@x.com \ --country-code +91 --phone 9876543210 \ --role agent --all read_write splashify team verify <member_id> --otp 123456 # if you used --no-verify earlier splashify team resend-otp <member_id> # invalidate + re-send the create OTP splashify team resend-invite <member_id> # re-send the "Set Password" email splashify team update <member_id> --role manager # role and/or permission update splashify team set-role <member_id> manager # convenience — role only splashify team set-permissions <member_id> --all read_write # convenience — perms only splashify team delete <member_id> # permanent removal

Full guide: see the Team & Agents page for the complete command reference, the page-key list, the four ways to build a permissions map, the member-limit gate, and end-to-end workflows (CSV-driven invites, bulk pending-cleanup, etc.).

Broadcast commands

splashify broadcasts # list campaigns splashify broadcast <id> # campaign detail splashify broadcast stats # overall stats splashify broadcast create --name "May Sale" --template may_offer \ --audience-type segment --audience-id <segment-id> \ --schedule 2026-06-01T10:00:00Z

--audience-type is segment, tag, or all. Omit --schedule to send now.

Templates, analytics & wallet

splashify templates # list approved WhatsApp templates splashify analytics # message analytics summary splashify analytics trends # analytics trends splashify wallet # wallet balance splashify wallet transactions # wallet transaction history

Full guides: Analytics, Wallet.

Dashboard

The dashboard command mirrors the app’s home /dashboard page — WABA + phone status, setup checklist, plan, wallet, AI credits, and KYC, all in one consolidated read.

splashify dashboard # consolidated snapshot (default) splashify dashboard setup-status # just the setup checklist splashify dashboard whatsapp-status # just the WhatsApp / Meta status splashify dashboard kyc # KYC verification status

Full guide: Dashboard.

Profile

profile mirrors /profile — name, password, WhatsApp number (with OTP), avatar upload, and 2FA toggle. The CLI’s own bearer token is unaffected by any of these — see Access Tokens.

splashify profile # show splashify profile update --first-name "Alice" --last-name "Smith" splashify profile change-password --current <old> --new <new> splashify profile whatsapp send-otp --country-code +91 --mobile 9876543210 splashify profile whatsapp verify --country-code +91 --mobile 9876543210 --otp 123456 splashify profile picture ./avatar.png splashify profile 2fa enable | disable

Full guide: Profile.

Company details

splashify company # show current company profile splashify company update --company-name "Acme Pvt Ltd" \ --industry "E-commerce" \ --company-size 51-200 \ --website https://acme.com \ --country IN \ --state KA \ --pincode 560001 \ --timezone Asia/Calcutta

Full guide: Company details.

Business username

splashify username # current username + status splashify username suggestions # backend-suggested ideas splashify username adopt mybrand # claim a username splashify username delete # release the current username

Full guide: Business username.

Maya (AI assistant)

splashify maya chat "How do I create a broadcast?" splashify maya chat --thread-id <id> "follow-up question" splashify maya feedback --reply-id <id> --rating up|down [--comment "…"]

Full guide: Maya.

The generic api command — everything else

Any Splashify Pro app endpoint is reachable directly, so the CLI covers the entire app surface (segments, AI agents, team, tickets, WhatsApp setup, and more):

splashify api GET /app/segments splashify api GET "/app/contacts?page=2&page_size=50" splashify api POST /app/messages/send-text --data '{"phone":"+919876543210","message":"hi"}' splashify api POST /app/ai-agents --data '{"name":"Support Bot"}' splashify api DELETE /app/contacts/<id>
  • The path may be given with or without the /api/v1 prefix.
  • Use --data to pass a JSON request body for POST / PUT / PATCH.
  • Query parameters can be added directly in the path with ?key=value.

This guarantees full coverage — even endpoints without a dedicated friendly command can be called.

Output

Every command prints the backend’s JSON response, indented for readability. On failure, the CLI prints error: … and exits with a non-zero status, which makes it easy to use in scripts.

Next