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 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.
| 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. |
attachment_id, source | string | No | Optional 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"}
]
}
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.
| 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