Skip to main content

Auto-Reply Rules

A comment rule answers four questions and nothing more:

  1. Which posts? All posts on the channel, or a list you pick.
  2. Which comments? Every comment, or comments matching keywords.
  3. What happens? Send a private reply, or start a flow.
  4. Is the message different per post? Optionally yes.

Rules run inside SendSeven, so a comment is answered within seconds without your integration being in the path at all. The comment.received webhook still fires either way.

Endpoints​

GET    /api/v1/social-comment-rules            # list, in evaluation order
POST /api/v1/social-comment-rules # create
GET /api/v1/social-comment-rules/{rule_id} # read
PATCH /api/v1/social-comment-rules/{rule_id} # update
DELETE /api/v1/social-comment-rules/{rule_id} # delete
OperationScope
GETflows:read
POST, PATCH, DELETEflows:write

Comment rules are automation, so they use the same scopes as the flow builder.

Plan tiers​

Two different plan features gate this endpoint, and they are not the same one:

FeatureCoversIncluded on
social_commentsThe Comments API, manual private replies, and rules existing and firing at allBasic and above (not API Only)
flowsAnything that hands off to the flow builder: a start_flow rule, a start_flow button, a start_flow quick replyProfessional and above

So a Basic workspace can create rules, send private replies from them, and attach plain and URL buttons and quick replies — it just cannot start a flow from any of them. Build the UI to show the flow option disabled rather than hiding it.

If a workspace loses social_comments, the endpoints return 403 and existing rules stop firing — they are never left running where you cannot see or disable them.

If a workspace loses only flows, the rules keep firing. Stored start_flow buttons and quick replies are kept exactly as written — nothing is rewritten server-side, so GET still returns them and an upgrade restores them instantly — but they are dropped from each outgoing reply, and a tap on one already delivered before the downgrade does nothing. The rest of the reply (text, plain and URL buttons, plain chips) still goes out.

Anatomy of a rule​

FieldTypeRequiredDescription
channel_idstringYesThe Instagram or Facebook Messenger channel this rule watches. Immutable — moving a rule to another channel silently changes which posts it can match, so that is a delete plus a create.
namestringYesYour own label, up to 255 characters.
enabledbooleanNoDefault true. A disabled rule is skipped entirely.
priorityintegerNo0–1000, default 0. Lower runs first.
post_scopestringNoall_posts (default) or specific_posts.
post_idsstring[]ConditionalRequired when post_scope is specific_posts. Up to 200. These are the platform's own post ids — the post.external_id value from comment.received.
match_modestringNoany_comment (default), contains, exact, or starts_with.
keywordsstring[]ConditionalRequired unless match_mode is any_comment. Up to 50, each up to 100 characters. Case-insensitive; trimmed, de-duplicated and blank-stripped on save.
actionstringNosend_private_reply (default) or start_flow.
reply_textstringConditionalThe private reply to send, up to 1000 characters. Required for send_private_reply unless every watched post has an override.
reply_buttonsarrayNoFacebook Messenger rules only. Up to 3 buttons under the reply. See Buttons.
reply_quick_repliesarrayNoFacebook Messenger rules only. Up to 13 quick-reply chips above the keyboard. Can be combined with reply_buttons. See Quick replies.
flow_idstringConditionalRequired for start_flow.
post_message_overridesobjectNo{post_id: message} — a different message per post. Up to 200 entries, each up to 1000 characters.

Matching​

match_modeMatches when
any_commentAlways — no keywords needed. Also matches comments with no text at all, such as a sticker or media-only comment.
containsAny keyword appears anywhere in the comment.
exactThe comment equals a keyword, after trimming.
starts_withThe comment begins with a keyword.

Matching is case-insensitive, and any one keyword matching is enough. The three keyword modes require the comment to have text.

Your own comments never trigger a rule

Meta echoes your account's own comments — including your public replies — back as ordinary comment webhooks. Rules always exclude them. This is a product invariant, not a setting: without it, a catch-all rule would DM itself in a loop.

Evaluation: exactly one rule acts​

When a comment arrives, SendSeven asks for the first matching enabled rule on that channel:

  1. priority ascending — lower runs first.
  2. Ties broken by specificity: a post-scoped, keyworded rule beats a post-scoped one, which beats a keyworded one, which beats a catch-all.
  3. Remaining ties broken by age (oldest first), so the order is fully deterministic — the same comment always picks the same rule.

The first match wins and no further rule is evaluated. GET /social-comment-rules returns rules in exactly this order, so the list reads top-to-bottom the way it behaves.

A matched rule suppresses comment_received flow triggers

Meta allows exactly one private reply per comment, ever. If a rule and a comment_received flow trigger both fired for the same comment, one of the two DMs would fail — and which one would be a race. So the contract is rules first, flow triggers as the fallback:

  • A rule matched → that rule acts, and comment_received flow triggers are not dispatched for that comment.
  • No rule matched → comment_received flow triggers run exactly as they did before rules existed.

A workspace with no rules therefore behaves identically to one where rules do not exist. To combine the two, use a rule with the start_flow action instead of relying on the flow's own trigger.

A rule also does not act when the comment falls outside Meta's 7-day private-reply window, or when the event is a redelivery of a comment that was already recorded. In both cases the comment simply stays unanswered.

Action: send a private reply​

curl -X POST "https://api.sendseven.com/api/v1/social-comment-rules" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "ch_ig_001",
"name": "Pricing questions",
"priority": 0,
"post_scope": "all_posts",
"match_mode": "contains",
"keywords": ["price", "cost", "how much"],
"action": "send_private_reply",
"reply_text": "Hi {{contact.first_name}}! Our pricing is at example.com/pricing 🙂"
}'

201 Created returns the rule:

{
"id": "scr_a1b2c3d4",
"tenant_id": "tenant_abc123",
"channel_id": "ch_ig_001",
"name": "Pricing questions",
"enabled": true,
"priority": 0,
"post_scope": "all_posts",
"post_ids": null,
"match_mode": "contains",
"keywords": ["price", "cost", "how much"],
"action": "send_private_reply",
"reply_text": "Hi {{contact.first_name}}! Our pricing is at example.com/pricing 🙂",
"reply_buttons": null,
"reply_quick_replies": null,
"flow_id": null,
"post_message_overrides": null,
"created_at": "2026-03-04T15:00:00Z",
"updated_at": "2026-03-04T15:00:00Z"
}

A rule's reply can carry buttons and quick replies on both platforms, together or on their own. Images (Facebook only) stay on the manual reply endpoint and the flow builder.

Buttons​

On a Facebook Messenger or Instagram rule, reply_buttons puts up to three buttons under the reply. Each button either opens a link or does something when tapped — including starting one of your flows, which turns a one-shot auto-reply into a real conversation without writing any code.

FieldTypeRequiredDescription
titlestringYesThe label, max 20 characters (Meta's limit).
typestringNopostback (default) or url.
urlstringYes for urlAbsolute https:// link.
actionstringNostart_flow. Requires flow_id.
flow_idstringYes for start_flowThe flow the tap starts. Must be in the same workspace.
payloadstringNoOpaque value you receive as meta.button_id when a plain postback button is tapped. Defaults to title. Not accepted on url or start_flow buttons.
attachment_id, sourcestringNoOptional editor metadata on url buttons only (max 64 characters each) — for example the id of the uploaded file a link button points at. Stored with the rule and returned when you read it back; never sent to Meta.
{
"channel_id": "ch_fb_002",
"name": "Launch day",
"match_mode": "contains",
"keywords": ["code", "discount"],
"action": "send_private_reply",
"reply_text": "Thanks for commenting! Want the 10% launch code?",
"reply_buttons": [
{"title": "Yes, send it", "action": "start_flow", "flow_id": "flow_5f6a7b8c"},
{"type": "url", "title": "Browse the shop", "url": "https://example.com/shop"}
]
}
Buttons and quick replies work on both platforms

reply_buttons and reply_quick_replies are fully supported on Facebook and Instagram rules — verified on live accounts: the controls render and taps arrive back as postbacks. The only remaining platform difference is reply_image_url, which stays Facebook-only.

Quick replies​

reply_quick_replies puts up to 13 tappable chips above the keyboard, on both Facebook Messenger and Instagram. They are not an alternative to buttons — a reply may carry both. Buttons sit under the message and persist in the thread; chips sit above the keyboard and disappear once one is tapped, which makes them the better fit for a one-off question.

FieldTypeRequiredDescription
titlestringYesThe chip label, max 20 characters (Meta's limit).
content_typestringNotext — the only kind supported. Omit it and text is assumed.
actionstringNostart_flow. Requires flow_id.
flow_idstringYes for start_flowThe flow the tap starts. Must be in the same workspace.
payloadstringNoOpaque value you receive as meta.button_id when the chip is tapped — the same field a button postback uses, so one handler covers both. Defaults to title. Not accepted on start_flow chips.
{
"channel_id": "ch_fb_002",
"name": "Size question",
"match_mode": "contains",
"keywords": ["size", "fit"],
"action": "send_private_reply",
"reply_text": "Happy to help! Which size are you after?",
"reply_quick_replies": [
{"title": "Small", "payload": "size_s"},
{"title": "Medium", "payload": "size_m"},
{"title": "Talk to a human", "action": "start_flow", "flow_id": "flow_5f6a7b8c"}
]
}

A chip has no link variant — that is Meta's model, not ours. Use a url button when you need to send someone to a page.

A chip's flow follows the same rule as a button's flow, including the Comment received exclusion.

Buttons that start a flow​

A start_flow button is the mirror image of the start_flow action, and the two want opposite flows:

start_flow actionstart_flow button
When it runsThe moment a comment matchesOnly if the person taps
Flow trigger requiredMust be Comment receivedAnything except Comment received
Who sends the first messageThe flow's own Send nodeThe rule's reply_text

The reason for the second row: a Comment received flow answers with a private reply of its own, and the comment's single allowed reply has already been spent on the message carrying the button. Point button flows at a different trigger — Manual is the usual choice — and they send normally in the conversation the tap opens.

The flow is checked when you save the rule (it must exist in your workspace with a permitted trigger) and again when someone taps. If it was deleted or unpublished in between, the tap does nothing — but the reply itself still goes out, and later taps work again once the flow is published.

A tapped button is not a reply to your flows

A flow waiting on an "Ask with buttons" step ignores a start_flow tap — the tap starts its own run instead of resolving that wait.

Variables​

reply_text and per-post messages support the same {{placeholders}} as the flow Send node, plus three comment-specific ones:

VariableValue
{{comment.text}}The comment body.
{{comment.author}}The commenter's handle (Instagram) or display name.
{{post.caption}}The post's caption. May be empty.
{{contact.first_name}}, {{contact.last_name}}, {{contact.custom.<field>}}, …The usual contact and custom-field variables.

An unknown variable renders as an empty string — it is never left in the message as literal {{...}}.

Substitution is single-pass, and that matters here

Each {{token}} in your template is resolved once, and the result is never re-scanned. Part of the input — {{comment.text}} — is written by a member of the public. If someone comments the literal text {{contact.email}}, echoing the comment back returns that string verbatim rather than substituting anything.

Per-post messages​

One rule, many posts, tailored wording. post_message_overrides is keyed by the platform's post id; reply_text is the fallback for any post without an override.

{
"channel_id": "ch_ig_001",
"name": "Product launches",
"post_scope": "specific_posts",
"post_ids": ["17998877665544332", "17998877665544333"],
"match_mode": "any_comment",
"action": "send_private_reply",
"reply_text": "Thanks for commenting! Here is the link: example.com/shop",
"post_message_overrides": {
"17998877665544332": "Thanks! The blue edition ships Monday: example.com/blue",
"17998877665544333": "Thanks! The red edition is live now: example.com/red"
}
}

You may omit reply_text only if the rule is scoped to specific_posts and every listed post has an override — otherwise a comment on an uncovered post would silently do nothing, which reads as a broken rule. That combination is rejected with 422.

Action: start a flow​

send_private_reply sends exactly one message. For anything that needs a back-and-forth — collect an email address, ask a qualifying question, branch on the answer — hand the comment to a flow instead:

curl -X POST "https://api.sendseven.com/api/v1/social-comment-rules" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "ch_ig_001",
"name": "Webinar signup",
"priority": 10,
"post_scope": "specific_posts",
"post_ids": ["17998877665544332"],
"match_mode": "exact",
"keywords": ["webinar"],
"action": "start_flow",
"flow_id": "flow_5f6a7b8c"
}'

The flow's first Send node goes out as the private reply, and everything after it is an ordinary DM conversation — the recipient's answer opens the standard messaging window, so later steps can use buttons, media and Collect Input freely, on both platforms.

The comment_received requirement​

The target flow's trigger must be comment_received. A flow's trigger type is what tells the engine that its first message is a private reply rather than a normal DM; a flow with any other trigger would try to open a conversation that no messaging window permits.

SituationWhat happens
Flow's trigger is not comment_receivedRejected at save time with 422.
Flow's trigger is changed afterwardsThe rule stops starting it. The comment stays unanswered.
Flow is still a draftAllowed at save time — you may build the rule and the flow in either order — but nothing starts until the flow is published.
Flow was deletedThe rule does nothing; the comment stays unanswered.
A run is already active for that contactNormal flow arbitration applies. If it does not start, the comment stays unanswered.

The rule is the matcher, not the flow. A rule can point at a flow whose own trigger watches a different post, and the flow will run for the rule's post — that is the entire point of the hand-off.

The comment is claimed either way

When a start_flow rule fires, the comment is marked as auto-answered even though the flow's Send node owns the actual message. An agent must not reply on top of it: Meta permits exactly one private reply, so the manual attempt would fail with 409.

Managing rules​

List​

curl "https://api.sendseven.com/api/v1/social-comment-rules?channel_id=ch_ig_001&enabled=true" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"

Returns {"items": [...], "pagination": {...}} with page / page_size (default 20, max 100), in evaluation order. Filter with channel_id and enabled.

Update​

PATCH is partial — omitted fields are left alone, so you can toggle a rule without restating it:

curl -X PATCH "https://api.sendseven.com/api/v1/social-comment-rules/scr_a1b2c3d4" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'

The merged rule is re-validated, not just the fields you sent: switching action to start_flow without also sending a flow_id fails, even though each field looks fine on its own. channel_id cannot be patched, and unknown fields are rejected rather than ignored.

Delete​

curl -X DELETE "https://api.sendseven.com/api/v1/social-comment-rules/scr_a1b2c3d4" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"
{"success": true, "id": "scr_a1b2c3d4", "message": "Rule deleted"}

Deleting a rule does not undo anything it already did — the comments it answered keep their state and their reply messages.

Errors​

StatusMeaning
403Comments are not enabled for this workspace, or your plan does not include social_comments (API Only). A plan without flows does not produce a 403 here — it produces the flow_start_requires_plan 422 below.
404Unknown rule, or a channel_id that is not yours. A channel belonging to someone else returns 404, not 403.
422The rule could never do anything: specific_posts with no post_ids, a keyword mode with no keywords, a reply action with no message, start_flow with no flow_id, or a flow_id whose trigger is not "Comment received". The message says which.
422The channel is not an Instagram or Facebook Messenger channel.
422A button is invalid — too many, a label over 20 characters, a link that is not absolute https, an unknown flow_id, a flow whose trigger cannot be started from a button, or a button on a channel whose platform does not support them. The body carries error_code: "invalid_buttons" and an errors array of {index, field, reason}; see the manual endpoint's invalid_buttons detail.
422A quick reply is invalid — too many, a label over 20 characters, a link on a chip (Meta has no URL chip), an unknown flow_id, or a chip on a channel whose platform does not support them. Same envelope under error_code: "invalid_quick_replies"; see invalid_quick_replies detail.
422A button or quick reply uses action: "start_flow" on a plan that does not include flows. Reason code flow_start_requires_plan on the flow_id field, under whichever of invalid_buttons / invalid_quick_replies applies. Checked before the flow lookup, so a real flow id is never reported as unknown_flow when the real answer is the plan.

Billing​

Every private reply a rule sends is a billed outbound message — see Overview → Billing for why a broad rule on a popular post deserves a keyword filter.

Next steps​