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:

ValueMeaningSuggested action
isFollower === trueLookup reports the person follows this accountRelease the resource if campaign eligibility still holds.
isFollower === falseLookup reports they do not followExplain the requirement and offer one deliberate recheck.
sender / author is null, or isFollower is nullNo usable follow result, including consent or lookup failuresSay 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.id in message.received; that response opens the 24-hour window. Quick replies are not shown on Instagram desktop, so include a typed alternative.
  • A postback button sends an interaction, also arriving as message.received, with interaction.type: "button". It can open the response window and trigger the check.
  • A url button 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.