Someone comments “LINK.” Your automation says it ran. Their inbox looks empty.
That symptom does not identify the failure. The comment might never have reached your endpoint. Your keyword rule might have skipped it. Instagram might have rejected the private reply. Or the message might be sitting in Requests while your application waits for an answer that has not happened.
Debug the boundaries, not the campaign. This guide traces an Instagram comment through the webhook, your application, the send request and the recipient's inbox. For the underlying feature and its limits, start with Instagram comment-to-DM automation.
Start with one comment and this decision tree
Use a fresh comment from a separate test account on a post owned by the connected Business or Creator account. Outstand skips comments written by the connected account itself. Start with a regular post or Reel, not Live, to avoid mixing different deadlines.
1Did your endpoint receive comment.received?
2 No -> Trace webhook configuration and delivery.
3 Yes -> Did your application select this comment?
4 No -> Trace keyword, post and eligibility filters.
5 Yes -> Did POST /v1/conversations succeed?
6 No -> Inspect HTTP status and the underlying error.
7 Yes -> Is the private reply visible to the recipient?
8 No -> Verify account, message IDs and Requests.
9 Yes -> Is the follow-up blocked?
10 Yes -> Verify an inbound reply and its window.Carry one correlation record through every stage. Suggested application log fields are traceId, UTC receivedAt, network, accountId, platformPostId, commentId, authorId, ruleVersion, decision, jobId, attempt, httpStatus, conversationId, messageId and platformMessageId. These are a logging design, not a promise that every webhook contains every field.
Keep identifiers as strings. Limit access and retention; use keyed hashes for identifiers in routine dashboards, retaining originals only where operationally necessary. Never log API keys, access tokens, signing secrets or complete account credential objects. Prefer a decision code and text length to storing someone's comment or DM.
1. No webhook: separate the two delivery legs
There are two subscriptions in an Outstand integration: Meta sends events to the integration's callback, then Outstand sends comment.received to your endpoint. A working HTTPS endpoint proves neither subscription is correct.
First, confirm your Outstand webhook is enabled, points to the intended environment and subscribes to comment.received. Its test button sends an event: "test" payload. Passing that test proves endpoint reachability, not that Instagram comments are subscribed.
Next, check ingress logs for the real test comment. Record the request arrival time, signature-verification result, response status and processing duration. If an edge proxy received the request but your handler did not, investigate routing, authentication middleware or parsing locally before blaming Meta.
Verify configured signatures against the raw request body using the webhook signature instructions. Durably enqueue accepted work, then return a prompt 2xx; do not wait for the Instagram send before acknowledging delivery.
For your own Meta app, check the verified per-app callback, app-level comments subscription and connected-account subscription. For the reply loop, subscribe to both messages and messaging_postbacks at the Meta app level and confirm both are subscribed for the connected account. Postback-button replies need messaging_postbacks, not just messages. Managed Keys remove the customer-owned app setup, not your own endpoint configuration. Follow the current Instagram configuration guide, including production access requirements.
Check permissions for the login you actually use
Do not copy a Facebook Login permission list into an Instagram Login connection:
Integration path | Private-reply requirements in Meta's guide |
|---|---|
Instagram Login | Instagram User token; |
Facebook Login | Page token; |
Those are private-reply requirements, not a complete messaging setup. Outstand's Instagram reply loop needs instagram_business_manage_messages to receive and manage DMs. It is not requested by default. Reconnect with the complete required scope list: Outstand's scopes parameter replaces defaults rather than adding to them. Enable Allow access to messages too.
Meta also documents a Unified Login path with its own requirements. Use the column matching your implementation in Meta's Private Replies guide, rather than treating any permission name as universal.
2. Webhook received, no keyword match: inspect your decisions
Keyword selection is your application logic. Outstand delivers the comment; your rule decides whether to send.
Log each exit separately: wrong_network, post_not_selected, nested_comment, keyword_miss, already_processed or selected. One undifferentiated “ignored” log hides the evidence you need.
This small JavaScript classifier makes two policies explicit: top-level Instagram comments only, with a case-insensitive whole-word match.
1function classify({ event, data }) {
2 if (event !== 'comment.received') return 'wrong_event';
3 if (data.network !== 'instagram') return 'wrong_network';
4 if (data.parentCommentId !== null) return 'nested_comment';
5 return /\blink\b/i.test(data.text ?? '')
6 ? 'selected'
7 : 'keyword_miss';
8}LINK please! matches; links does not. That is this rule's behavior, not an Instagram limitation. Test campaign wording against the actual matcher, including punctuation and any normalization you intentionally apply.
Do not require postId merely to send a private reply. Outstand can deliver comments on posts published outside its system, where that internal ID is null. Use platformPostId for an external-post allowlist and preserve accountId, commentId and authorId for the send.
Deduplicate work by accountId plus commentId, but distinguish queued, sending, successful and failed attempts. Marking a comment “done” before the job runs can turn a worker crash into a permanent skip.
3. Send rejected: read both error layers
The Outstand request maps identifiers directly from the event:
1const body = {
2 social_account_id: data.accountId,
3 content: 'Here is the guide: https://example.com/guide',
4 instagram: {
5 comment_id: data.commentId,
6 author_id: data.authorId,
7 },
8};Send that JSON to POST https://api.outstand.so/v1/conversations with your API key. This is a private reply addressed by comment, not a cold DM addressed by username. See the request reference.
Interpret Outstand statuses at their own layer:
Status | Diagnostic meaning |
|---|---|
| Invalid body or missing Instagram target fields. Fix the request. |
| Inspect API authentication or organization/key permissions, including |
| Account not found, or comment unavailable on that account. Verify ownership, deletion and identifiers. |
| Outstand already has a private-reply record for that comment. Reconcile the existing attempt rather than sending again. |
| An expired private-reply deadline, or a follow-up eligibility failure. Read the error text. |
| An upstream/credential operation failed. Inspect the wrapped error; this status alone does not prove a Meta outage. |
For regular posts and Reels, Meta allows one private reply per comment within seven days of comment creation. instagram_private_reply_window_expired is Outstand's age-check error. Queue receipt time does not restart that clock. Live is stricter: Meta permits private replies only during the broadcast. Receiving a Live comment event does not guarantee an integration supports sending its private reply.
When available, capture Meta's error.type, error.code, error.error_subcode, sanitized error.message and error.fbtrace_id, alongside the outer HTTP status and API version. Outstand may carry that response inside its error string rather than as separate fields.
In Meta's error reference, code 190 indicates invalid or expired token credentials; 10 and 200-series errors indicate permission problems. These are Graph error codes, not HTTP statuses. Reauthorization or corrected grants may be required; retrying unchanged credentials does not repair them.
For a timeout or uncertain send outcome, reconcile before replaying. Back off only where the actual transient or throttling response warrants it, with bounded attempts inside the remaining deadline. Do not retry every 502 indiscriminately.
4. Accepted, but not visible: verify the recipient's view
Outstand's successful Instagram private reply returns 201 with the message recorded as sent. Store its conversation ID, message ID and platform message ID. Those identify a successful send, not a human viewing it.
Neither 201 nor message.sent is a read receipt.
Confirm the commenter and connected sender are the accounts you intended. Then inspect the recipient's Instagram app. Meta says the private reply appears in the Inbox if the recipient follows the professional account, or in Requests if they do not.
If the sender-side record and recipient-side display disagree, preserve the UTC timestamp and message identifiers for support. Do not “test delivery” by repeatedly sending the same private reply. A duplicate rejection supplies no evidence about whether the recipient saw the original.
5. Follow-up blocked: check for an actual inbound reply
A comment and your private reply do not open the ordinary messaging window. For a contact who has never messaged the account, lastInboundAt remains null. Outstand rejects follow-ups with 422 and an error beginning instagram_no_inbound_message.
A typed answer, quick-reply tap or postback-button tap can provide the inbound message. A URL-button click does not. If the person answered but your app stayed silent, check both messages and messaging_postbacks at the Meta app level and in the connected account's subscriptions, then trace messaging permission, message.received delivery and conversation mapping. For a postback-button tap, specifically trace messaging_postbacks: Outstand converts the postback into an inbound message, with its payload in interaction.id. See Meta's button-template requirements.
Once an inbound reply is recorded, ordinary follow-ups have a 24-hour window from the contact's last message. Your outbound messages do not extend it. instagram_24h_window_expired means that window has closed. HUMAN_AGENT is not an automated nurture-sequence workaround.
Fix the first broken boundary
An escalation should include one pseudonymized trace, UTC timestamps, the last successful stage, the first failing stage, sanitized error details and restricted-access message identifiers. That separates your filter or queue failure from authorization problems and platform behavior.
For implementation, use the comment-to-DM walkthrough and troubleshooting reference. For the product overview, see Outstand's comment-to-DM API. Repair the failed boundary, then verify one fresh comment all the way to a recipient reply.