Auto-Reply Rules
A comment rule answers four questions and nothing more:
- Which posts? All posts on the channel, or a list you pick.
- Which comments? Every comment, or comments matching keywords.
- What happens? Send a private reply, or start a flow.
- 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
| Operation | Scope |
|---|---|
GET | flows:read |
POST, PATCH, DELETE | flows: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:
| Feature | Covers | Included on |
|---|---|---|
social_comments | The Comments API, manual private replies, and rules existing and firing at all | Basic and above (not API Only) |
flows | Anything that hands off to the flow builder: a start_flow rule, a start_flow button, a start_flow quick reply | Professional 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
| Field | Type | Required | Description |
|---|---|---|---|
channel_id | string | Yes | The 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. |
name | string | Yes | Your own label, up to 255 characters. |
enabled | boolean | No | Default true. A disabled rule is skipped entirely. |
priority | integer | No | 0–1000, default 0. Lower runs first. |
post_scope | string | No | all_posts (default) or specific_posts. |
post_ids | string[] | Conditional | Required 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_mode | string | No | any_comment (default), contains, exact, or starts_with. |
keywords | string[] | Conditional | Required unless match_mode is any_comment. Up to 50, each up to 100 characters. Case-insensitive; trimmed, de-duplicated and blank-stripped on save. |
action | string | No | send_private_reply (default) or start_flow. |
reply_text | string | Conditional | The private reply to send, up to 1000 characters. Required for send_private_reply unless every watched post has an override. |
reply_buttons | array | No | Facebook Messenger rules only. Up to 3 buttons under the reply. See Buttons. |
reply_quick_replies | array | No | Facebook Messenger rules only. Up to 13 quick-reply chips above the keyboard. Can be combined with reply_buttons. See Quick replies. |
flow_id | string | Conditional | Required for start_flow. |
post_message_overrides | object | No | {post_id: message} — a different message per post. Up to 200 entries, each up to 1000 characters. |
Matching
match_mode | Matches when |
|---|---|
any_comment | Always — no keywords needed. Also matches comments with no text at all, such as a sticker or media-only comment. |
contains | Any keyword appears anywhere in the comment. |
exact | The comment equals a keyword, after trimming. |
starts_with | The 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.
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:
priorityascending — lower runs first.- Ties broken by specificity: a post-scoped, keyworded rule beats a post-scoped one, which beats a keyworded one, which beats a catch-all.
- 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.
comment_received flow triggersMeta 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_receivedflow triggers are not dispatched for that comment. - No rule matched →
comment_receivedflow 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 Facebook Messenger, together or on their own. Images stay on the manual reply endpoint and the flow builder.
Buttons
On a Facebook Messenger 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.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | The label, max 20 characters (Meta's limit). |
type | string | No | postback (default) or url. |
url | string | Yes for url | Absolute https:// link. |
action | string | No | start_flow. Requires flow_id. |
flow_id | string | Yes for start_flow | The flow the tap starts. Must be in the same workspace. |
payload | string | No | Opaque 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. |
{
"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"}
]
}
Meta documents rich private replies for Facebook Messenger only. It says nothing either way about Instagram. reply_buttons and reply_quick_replies on an Instagram rule are currently accepted, as a live test of whether Meta actually renders them — not because Meta granted it.
Until that is confirmed, do not build an Instagram rule whose behaviour depends on the controls being tapped: Meta discards fields it does not recognise and still returns success, and a comment gets exactly one private reply ever, so a dropped control can never be resent. If the test fails, these will go back to being rejected with 422 at save time and existing rules will send their text without the controls.
None of this applies once the recipient answers — the standard messaging window then opens and both work normally on Instagram, which is what a start_flow rule gives you.
Quick replies
reply_quick_replies puts up to 13 tappable chips above the keyboard. Fully supported on Facebook Messenger. On Instagram this is in testing. Meta documents neither support nor a restriction for it, so we are verifying it on live accounts; it may be withdrawn. 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.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | The chip label, max 20 characters (Meta's limit). |
content_type | string | No | text — the only kind supported. Omit it and text is assumed. |
action | string | No | start_flow. Requires flow_id. |
flow_id | string | Yes for start_flow | The flow the tap starts. Must be in the same workspace. |
payload | string | No | Opaque 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 action | start_flow button | |
|---|---|---|
| When it runs | The moment a comment matches | Only if the person taps |
| Flow trigger required | Must be Comment received | Anything except Comment received |
| Who sends the first message | The flow's own Send node | The 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 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:
| Variable | Value |
|---|---|
{{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 {{...}}.
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.
| Situation | What happens |
|---|---|
Flow's trigger is not comment_received | Rejected at save time with 422. |
| Flow's trigger is changed afterwards | The rule stops starting it. The comment stays unanswered. |
| Flow is still a draft | Allowed at save time — you may build the rule and the flow in either order — but nothing starts until the flow is published. |
| Flow was deleted | The rule does nothing; the comment stays unanswered. |
| A run is already active for that contact | Normal 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.
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
| Status | Meaning |
|---|---|
403 | Comments 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. |
404 | Unknown rule, or a channel_id that is not yours. A channel belonging to someone else returns 404, not 403. |
422 | The 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. |
422 | The channel is not an Instagram or Facebook Messenger channel. |
422 | A 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. |
422 | A 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. |
422 | A 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
- Send private replies — the manual path, with buttons, quick replies and images on Messenger
- Flows: Triggers and the API — building the flow a rule hands off to
- Listen for comments — observing every comment, rule or no rule