Instagram Automation
The instagram command (alias: ig) mirrors the /instagram-automation
page in the app. From the terminal you can connect your Instagram Login
account, list the posts on it, build comment-to-DM automation rules, send
DMs in existing conversations, sync historical chats from Meta, and read the
automation activity log.
Backed by /api/v1/app/instagram/*. Every call uses your stored oc_live_
token and acts as you — only the account owner can manage Instagram on a
team account.
Prerequisites. Your account must have the Instagram Automation feature enabled (the backend gates this with
EnsureFeatureEnabled). If your plan doesn’t include it, the CLI surfaces the standardplan_requiredupgrade prompt with the exact upgrade URL.
Quick start
# 1. Get the OAuth URL and open it in your browser
splashify instagram oauth-url
# 2. After the IG redirect, copy the `code` query parameter from the URL
splashify instagram connect --code <code>
# 3. Confirm
splashify instagram # show the connected account
splashify instagram media --limit 25 # see what you can target with rules
# 4. Build a rule
splashify instagram rule create \
--media-id 17841401234567890 \
--keyword "shipping" \
--dm-message "Hey! Free shipping today only — code SHIP10 ✨" \
--comment-reply "Sent you a DM 💌" \
--active true
# 5. Watch it fire
splashify instagram logs --limit 20How the connect flow works
The handshake is a three-leg dance and the CLI handles the server-side leg only — the in-browser step still needs a real browser because Instagram Login requires the user to authenticate on instagram.com:
splashify instagram oauth-urlreturns anauthorize_urlyou open in a browser. The URL embeds youruser_idasstateso the redirect attributes the connection to the right account.- Instagram redirects to
https://app.splashifypro.com/instagram/callbackwith?code=...&state=...in the query string. Copy thecodevalue. splashify instagram connect --code <code>exchanges the code for a long-lived (~60 day) access token server-side, subscribes our app to webhooks for comments + messages, and stamps the row inapp_ig_accounts.
After step 3, splashify instagram returns the full account record. Use
splashify instagram disconnect to undo (drops every rule and log too).
Command reference
splashify instagram — show the connected account
splashify instagram # default
splashify instagram account # alias
splashify instagram info # aliasReturns {success, account} where account is null when nothing’s
connected, or:
{
"ig_user_id": "17841401234567890",
"ig_username": "your.handle",
"ig_name": "Your Brand",
"ig_profile_pic": "https://…",
"account_type": "BUSINESS",
"connected_at": "2026-04-12T10:23:04Z",
"webhooks_subscribed_at": "2026-04-12T10:23:07Z",
"token_expires_at": "2026-06-11T10:23:04Z"
}| Backed by | GET /api/v1/app/instagram/account |
|---|
splashify instagram oauth-url
splashify instagram oauth-url
splashify instagram authorize-url # aliasReturns the URL to open in a browser plus the redirect_uri the backend
expects to handle the callback (https://app.splashifypro.com/instagram/callback
in production).
| Backed by | GET /api/v1/app/instagram/oauth-url |
|---|
splashify instagram connect --code <code>
Completes the OAuth handshake on the backend.
splashify instagram connect --code AQB0Lg…JaYHFq8KQ| Backed by | POST /api/v1/app/instagram/connect {code} |
|---|
Effects:
- Exchanges the code for a short-lived token, then upgrades to a long-lived (~60 day) token.
- Reads
/mefor username, profile pic, account type. - Persists the encrypted token in
app_ig_accounts. - Subscribes the app to webhooks for
comments+messagesevents on this IG user (best-effort — surfaces inwebhooks_subscribed_at).
splashify instagram disconnect
splashify instagram disconnect| Backed by | DELETE /api/v1/app/instagram/disconnect |
|---|
Unsubscribes webhooks, deletes the account row, and removes every rule + log entry for this user. There is no undo. To reconnect later, run the whole OAuth dance again — a fresh long-lived token is minted and webhooks re-subscribed.
splashify instagram media [--limit N]
splashify instagram media # default limit: 50
splashify instagram media --limit 25| Backed by | GET /api/v1/app/instagram/media?limit=N |
|---|
Proxies Meta’s /me/media listing. Each item has id, caption,
media_type, thumbnail_url, permalink, timestamp — exactly the
fields you pass to rule create as --media-id, --media-caption,
--media-thumbnail, --media-permalink.
splashify instagram logs [--limit N]
splashify instagram logs # newest 50
splashify instagram logs --limit 200| Backed by | GET /api/v1/app/instagram/logs?limit=N |
|---|
The activity feed — every comment-to-DM trigger gets a row here (matched keyword, sender username, rule id, status, error message if Meta refused). Use this to debug why a rule didn’t fire.
splashify instagram window --conversation <id>
splashify instagram window --conversation 1c5d2f5e-…| Backed by | GET /api/v1/app/instagram/messages/window?conversation_id=… |
|---|
Meta enforces a strict reply window for Instagram DMs:
- 0–24h since the user last messaged you — free-text DMs allowed.
- 24h–7d — only
HUMAN_AGENTtagged messages (the backend sets this automatically — you don’t pass it). - >7d — no outbound DMs at all until the user messages you again.
The composer in /messages calls this before every send. From the CLI,
use it as a pre-check so a dm won’t fail with HTTP 403.
Response shape:
{
"success": true,
"window": {
"can_reply": true,
"needs_human_agent": false,
"reply_window_ends_at": "2026-05-21T14:00:00Z",
"last_inbound_at": "2026-05-20T14:00:00Z"
}
}splashify instagram sync
splashify instagram sync| Backed by | POST /api/v1/app/instagram/sync-conversations |
|---|
Pulls historical Instagram conversations from Meta and back-fills them into
/messages. Useful right after connect so you have something to reply
to. Returns a {messages_synced, conversations_synced} summary.
splashify instagram dm — send a DM
# Text only
splashify instagram dm --conversation <conversation_id> --message "Hey!"
# Media only
splashify instagram dm --conversation <conversation_id> \
--media-url https://cdn.example.com/img.jpg --media-type image
# Both — IG renders them as two bubbles
splashify instagram dm --conversation <conversation_id> \
--message "Here's the file you asked for ↓" \
--media-url https://cdn.example.com/brochure.pdf --media-type image| Backed by | POST /api/v1/app/instagram/messages |
|---|
| Flag | Required? | Notes |
|---|---|---|
--conversation | yes | conversation_id from splashify conversations (must be an instagram channel row) |
--message | one of | Text body |
--media-url + --media-type | one of | Public media URL + image, video, or audio |
Validation:
- Refuses with HTTP 403 if the 7-day window has expired (
splashify instagram windowis the pre-check). - Refuses with HTTP 400 if the conversation isn’t an Instagram channel (i.e. it’s a WhatsApp conv).
- Refuses with HTTP 502 if Meta refuses the upload (file too large,
unsupported format, URL unreachable) — the CLI surfaces Meta’s exact
error code (subcode
2018007is the most common one).
Two bubbles. When you pass
--messageAND--media-url, IG’s/me/messagesAPI sends them as two separate messages. The CLI inserts two rows inapp_messagesmatching that — each with its ownwa_message_idso the echo webhook can dedup correctly.
Comment-to-DM rules
The rule / rules commands manage automation rules — when a comment
on a chosen IG post contains a trigger_keyword, the system sends the
commenter a DM (and optionally a public reply on their comment).
Anatomy of a rule
| Field | Required? | Notes |
|---|---|---|
media_id | yes | The IG post the rule attaches to. Get IDs from splashify instagram media |
trigger_keyword | yes | Lowercased server-side; matched case-insensitively against comment text |
dm_message | yes | The text DM sent on a match |
dm_media_url | optional | If set, also sends an image / video / audio bubble |
dm_media_type | with dm_media_url | image, video, or audio (defaults to image if unset) |
comment_reply | optional | Public reply left on the matching comment |
is_active | optional | Defaults to true |
media_caption / media_thumbnail / media_permalink | optional | Cached for the rules list UI — pass the values from media for a nicer display |
List rules
splashify instagram rules # full list, newest first
splashify instagram rules list # alias| Backed by | GET /api/v1/app/instagram/rules |
|---|
Response shape per rule:
{
"rule_id": "uuid",
"media_id": "17841401234567890",
"media_caption": "Spring sale — link in bio",
"media_thumbnail": "https://…",
"media_permalink": "https://www.instagram.com/p/…",
"trigger_keyword": "shipping",
"dm_message": "Hey! Free shipping today only…",
"dm_media_url": "",
"dm_media_type": "",
"comment_reply": "Sent you a DM 💌",
"is_active": true,
"triggered_count": 12,
"created_at": "2026-04-12T11:00:00Z",
"updated_at": "2026-04-12T11:00:00Z"
}Show one rule
splashify instagram rule <rule_id>The CLI fetches the list and filters client-side (no per-id GET on the backend).
Create a rule
splashify instagram rule create \
--media-id <media_id_from_media_list> \
--keyword "buy" \
--dm-message "Here's the link — https://shop.example.com/?utm=ig" \
--comment-reply "DM sent 💌" \
--active trueOptional flags:
splashify instagram rule create \
--media-id <id> --keyword "free" --dm-message "Use code FREE15 …" \
--dm-media-url https://cdn.example.com/coupon.jpg --dm-media-type image \
--media-caption "Spring drop — comment FREE for code"| Backed by | POST /api/v1/app/instagram/rules |
|---|
Update a rule
splashify instagram rule update <rule_id> --keyword "new-keyword"
splashify instagram rule update <rule_id> --dm-message "Updated body" --active false
splashify instagram rule update <rule_id> --dm-media-url "" --dm-media-type "" # clear the media| Backed by | PATCH /api/v1/app/instagram/rules/:rule_id |
|---|
Read-modify-write. The backend’s PATCH writes every editable column
on every call (trigger_keyword, dm_message, dm_media_url,
dm_media_type, comment_reply, is_active), so a partial flag set
would blank the others. The CLI loads the rule first, overlays only your
explicitly-passed flags, then submits the full body — fields you didn’t
mention stay intact. Same pattern as splashify canned update, waba update, opt ….
Toggle activate / deactivate
splashify instagram rule toggle <rule_id>There is no dedicated toggle endpoint for IG rules (unlike canned messages),
so the CLI implements this as load → flip is_active → PATCH. The output
is the standard PATCH response.
Delete a rule
splashify instagram rule delete <rule_id>
splashify instagram rule rm <rule_id> # alias| Backed by | DELETE /api/v1/app/instagram/rules/:rule_id |
|---|
Permanent. The row is also removed from the by-media index so the rule won’t fire on incoming comments.
End-to-end workflows
Bootstrap automation from scratch
# 1. Connect
splashify instagram oauth-url
# (open the URL, paste the code from the redirect)
splashify instagram connect --code AQB…
# 2. Discover targets
splashify instagram media --limit 50 | jq '.media[] | {id, caption: (.caption // "")[:80], permalink}'
# 3. Create one rule per target post (e.g. from a CSV)
# csv format: media_id,keyword,dm,comment_reply
while IFS=, read -r mid kw dm reply; do
splashify instagram rule create \
--media-id "$mid" --keyword "$kw" --dm-message "$dm" --comment-reply "$reply" --active true
done < ig-rules.csv
# 4. Verify
splashify instagram rules | jq '.rules[] | {rule_id, keyword: .trigger_keyword, active: .is_active}'Deactivate every rule on a specific post
TARGET_MEDIA=17841401234567890
splashify instagram rules | \
jq -r --arg m "$TARGET_MEDIA" '.rules[] | select(.media_id == $m) | .rule_id' | \
xargs -I{} splashify instagram rule toggle {}Find the rule that fired most often last week
splashify instagram rules | \
jq 'first(.rules | sort_by(.triggered_count) | reverse | .[]) |
{rule_id, keyword: .trigger_keyword, fired: .triggered_count, dm: .dm_message[:60]}'Replace the DM body on every active rule
NEW_DM="Updated promo — see https://shop.example.com/spring"
splashify instagram rules | \
jq -r '.rules[] | select(.is_active) | .rule_id' | \
xargs -I{} splashify instagram rule update {} --dm-message "$NEW_DM"Reply to an active conversation
# Find an open IG conversation
CONV=$(splashify conversations --status open | \
jq -r '.conversations[] | select(.channel == "instagram") | .id' | head -1)
# Pre-check the reply window
splashify instagram window --conversation "$CONV" | jq '.window'
# Send
splashify instagram dm --conversation "$CONV" --message "Thanks for the message!"Re-upload the welcome image + swap it on every rule
NEW_URL=$(splashify media upload ./welcome-v2.jpg | jq -r '.file_url')
splashify instagram rules | jq -r '.rules[] | select(.dm_media_url != "") | .rule_id' | \
xargs -I{} splashify instagram rule update {} --dm-media-url "$NEW_URL" --dm-media-type imageTroubleshooting
“Instagram is not configured on the server” — the partner backend
doesn’t have INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET set in its env.
Reseller-only — contact your provider.
splashify instagram returns { "account": null } — you haven’t
connected an IG account yet. Run the oauth-url → browser → connect
flow above.
splashify instagram connect returns “Could not complete Instagram
login” — the --code was already redeemed (codes are one-use), or the
backend’s INSTAGRAM_REDIRECT_URI doesn’t match the one configured in the
Meta app. Get a fresh code from oauth-url.
splashify instagram rule create returns “media_id, trigger_keyword
and dm_message are required” — at least one of those is empty after
trimming whitespace. The CLI catches this before the round-trip, but if
you only see it from --data / generic api you forgot a required field.
DM send returns “This user hasn’t messaged you in over 7 days …” — Meta’s 7-day window expired. Wait for the user to message first; there’s no override.
DM send returns “Instagram couldn’t accept this attachment” — Meta
subcode 2018007. The most common causes: image > 8MB, video > 25MB,
unsupported format (must be JPG/PNG/MP4/MP3/WAV), or the URL is
unreachable from Meta’s servers. Test the URL with curl -I first.
Rule fires but no DM arrives — check splashify instagram logs. If the
log row shows status: "skipped" the commenter is your own IG account
(self-comments never trigger). If it shows a Meta error code, the access
token may have expired — reconnect.
Webhook subscription stuck — the response after connect shows
webhooks_subscribed_at: null. The subscribe is best-effort + async;
re-run splashify instagram connect --code <code> (with a fresh code)
to retry, or wait — every successful send refreshes the subscription.
See also
splashify conversations— get conversation IDs to pass toinstagram dmandinstagram window.splashify media upload— upload files first to get CDN URLs you can use as--dm-media-urlor--media-url.splashify message— the WhatsApp equivalent of these DM commands.