Instagram follow-to-unlock
Build a two-stage Instagram follower check using a private reply, a real inbound response and nullable follow-state enrichment.
An Instagram follow-to-unlock campaign asks a commenter to follow the account before receiving a resource. The difficult part is not sending the link: it is distinguishing following, not following and unknown without sending unsolicited follow-ups.
This recipe uses existing Outstand comment and messaging APIs. Complete Comment-to-DM setup and the durable pipeline first. It is not a separate hosted automation product or a new login flow.
For Instagram Login BYOK, enable both messages and messaging_postbacks at the Meta app level. Connect or reconnect with instagram_business_manage_messages so Outstand subscribes the account to both fields too. Quick replies use messages; postback buttons use messaging_postbacks. Subscribe your Outstand endpoint to message.received and test both paths. Managed Keys handle the app-level configuration. See BYOK messaging setup.
Why the flow has two stages
Meta requires user consent before its Instagram User Profile API can return profile information. A comment alone does not grant that access. For most first-time commenters, Outstand's comment.received.data.author is therefore null.
Outstand can enrich comments for people who have already messaged the account. When a new inbound message arrives, it looks up the sender again, so message.received.data.sender can reflect a follow made since the comment. The normalized isFollower field comes from Meta's is_user_follow_business.
Neither the presence of a comment nor a "I'm following" button tap proves a follow. Use the enriched boolean, and keep unknown separate:
| Value | Meaning | Suggested action |
|---|---|---|
isFollower === true | Lookup reports the person follows this account | Release the resource if campaign eligibility still holds. |
isFollower === false | Lookup reports they do not follow | Explain the requirement and offer one deliberate recheck. |
sender / author is null, or isFollower is null | No usable follow result, including consent or lookup failures | Say verification is unavailable. Choose a documented fallback, not an accusation that they do not follow. |
This is a snapshot, not a guarantee that someone will keep following. Do not promise real-time follow monitoring or an automatic notification when a follow occurs.
Stage 1: answer the qualifying comment privately
Apply account/post scope, keyword rules, comment deduplication and cooldowns first. If author?.isFollower === true, you may send the promised resource in the first private reply. Otherwise send one invitation:
curl -X POST https://api.outstand.so/v1/conversations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"social_account_id": "x8Kp2",
"content": "Follow our account, then tap below to check and get the guide. You can also reply CHECK.",
"instagram": {
"comment_id": "17865799348089039",
"author_id": "17841400000000000",
"quick_replies": [
{ "title": "Check follow", "id": "FOLLOW_CHECK_GUIDE_V1" }
]
}
}'Save the resulting conversation.id against the tenant, account, person, campaign and source comment. Create a durable pending-unlock record so a later inbound tap cannot select an unrelated campaign. Use server-side state; an interaction ID is routing data, not authorization. Define how to resolve multiple pending campaigns in one conversation.
The private reply consumes that comment's one initial reply. Do not send a second reminder until the person responds. Meta's private-reply rules still apply: seven days for ordinary comments, only during a live broadcast for Instagram Live, and no follow-up window until an inbound response.
Quick reply, postback or URL?
- A quick reply sends its title as an inbound message. Outstand includes
interaction.idinmessage.received; that response opens the 24-hour window. Quick replies are not shown on Instagram desktop, so include a typed alternative. - A
postbackbutton sends an interaction, also arriving asmessage.received, withinteraction.type: "button". It can open the response window and trigger the check. - A
urlbutton opens a link only. Opening your profile or a resource does not produce the inbound response this workflow needs, does not open the window and does not verify a follow.
For persistent buttons, replace quick_replies with:
{
"buttons": [
{ "type": "url", "title": "Open our profile", "url": "https://www.instagram.com/YOUR_ACCOUNT/" },
{ "type": "postback", "title": "Check follow", "id": "FOLLOW_CHECK_GUIDE_V1" }
]
}Put this object inside instagram alongside the two IDs. Do not combine buttons and quick replies. Buttons permit up to three entries and limit content to 640 characters; quick replies allow up to 13 entries with titles up to 20 characters. See message lifecycle.
Stage 2: process a real inbound response
Verify, persist and acknowledge message.received before processing. Deduplicate by organization and message ID, then load the authorized pending-unlock record. A quick reply or postback might look like this abridged data:
{
"conversationId": "9dyJS",
"messageId": "a7BcD",
"orgId": "YOUR_ORG_ID",
"network": "instagram",
"senderId": "17841400000000000",
"interaction": { "type": "quick_reply", "id": "FOLLOW_CHECK_GUIDE_V1" },
"sender": { "isFollower": true }
}The following is worker-side application pseudocode, after durable ingestion. Storage and route helpers are yours to implement:
async function planUnlockResponse(data, pending) {
if (data.network !== 'instagram') return null;
if (!pending || pending.orgId !== data.orgId || pending.conversationId !== data.conversationId) return null;
if (pending.participantId !== data.senderId || pending.fulfilled || pending.expired) return null;
const requestedCheck = data.interaction?.id === 'FOLLOW_CHECK_GUIDE_V1'
|| data.content?.trim().toUpperCase() === 'CHECK';
if (!requestedCheck) return null;
if (data.sender?.isFollower === true) {
return { content: `Thanks for following! Here is your guide: ${pending.resourceUrl}` };
}
if (data.sender?.isFollower === false) {
return {
content: 'The latest check says you are not following yet. Follow us, then tap Check follow or reply CHECK.',
instagram: { quick_replies: [{ title: 'Check follow', id: 'FOLLOW_CHECK_GUIDE_V1' }] },
};
}
return { content: 'We could not verify your follow status right now. Reply HELP for assistance.' };
}The pending record's participant ID comes from your saved conversation mapping, not a client-supplied value. Apply an atomic release reservation and a limit on recheck responses before sending the planned body with:
curl -X POST https://api.outstand.so/v1/conversations/CONVERSATION_ID/messages \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Thanks for following! Here is your guide: https://example.com/guide"}'Only mark fulfilled after a confirmed successful send. Retain a separate unknown_outcome state if the response is lost, so another tap does not automatically release the resource twice. Keep the reservation until reconciliation. Repeated taps and webhook redeliveries need independent deduplication: a new inbound message ID is not permission to fulfill an already-fulfilled campaign again.
Unknown-state and window policy
Choose and document one fallback for unknown state: deliver the resource anyway, offer human assistance, or allow a bounded user-requested recheck. The example uses assistance. Never convert null into false, never tell an unknown user that a follow failed, and never poll/send repeated prompts without an inbound response.
A recheck prompt itself is an outgoing message and must remain within the current 24-hour response window. If a delayed worker encounters a closed window, stop and wait for a new inbound message. A profile follow or URL click alone does not reopen it. Refresh the check on a new inbound event rather than relying indefinitely on a cached boolean.
Other networks
Facebook's normalized profile does not expose follow status for this flow. Do not copy the Instagram gate to Facebook or interpret null as a Page non-follower. X exposes follower information through its own lookup and uses ordinary DM rules; do not reuse Instagram's consent and messaging-window assumptions there.
For missing taps, unavailable sender data or 422 follow-ups, use troubleshooting.