Instagram story reply webhooks — payload schema showing reply_to.story, CDN URL expiry warning, messaging_referrals for story mentions, auto-respond pipeline
In this guide: Story reply vs story mention — the distinction that matters · Exact webhook payload schemas for both · The messaging_referrals subscription · CDN URL expiry and how to handle it · The 24-hour window for auto-responses · Full auto-respond pipeline code (Node.js + Python) · Use cases and templates · SocialHook normalized format

Two distinct events: story reply vs story mention

The first thing to understand: "Instagram story interactions" covers two completely different webhook events with different payload structures, different webhook subscriptions, and different triggered actions. Confusing them is the #1 source of bugs in story automation systems.

Story Reply
User replies to YOUR story
"You post a story. A follower swipes up and sends 'This is amazing!' in DM reply to your story."
Subscription: messages
Event type: message event
Detection: event.message.reply_to.story
Payload path: event.message.text (their reply)
Story URL: event.message.reply_to.story.url
Story Mention
User mentions YOU in THEIR story
"A customer posts a story showing your product and tags your business account with @yourbrand."
Subscription: messaging_referrals
Event type: referral event
Detection: event.referral.source === 'STORY_MENTION'
Payload path: event.referral.story.url
Story ID: event.referral.story.id

Webhook subscriptions: which fields you need

This is the most commonly missed setup detail — each story event type requires its own webhook field subscription. Both must be enabled in your Facebook App → Instagram → Webhooks → Edit Subscriptions.

FieldRequired forWhat you miss without it
messages Required All DMs including story replies. Without this, you receive no message events whatsoever — no text DMs, no story replies, no media messages. This is the base subscription.
messaging_referrals Required Story mentions (when users tag you in their own stories) and Click-to-DM ad entries. Without this, story mentions silently disappear — no error, no event, just nothing.
messaging_optins Optional Opt-in events when users tap Allow on a recurring notification opt-in widget. Not needed for story interactions.
Subscribe to both right now. If you currently only have messages subscribed, you've already missed every story mention that came to your account. There's no way to retrieve them retroactively — the events fired and were lost. Add messaging_referrals in your App settings immediately after reading this section.

Story reply: exact webhook payload

When a user replies to your Instagram Story via DM, the event arrives as a standard messages webhook event — but with an additional reply_to object nested inside the message. This is how you distinguish a story reply from a regular DM.

Story reply — full webhook payload
{ "object": "instagram", "entry": [{ "id": "987654321098765", // your IG Business Account ID "messaging": [{ "sender": { "id": "12345678901234" }, // IGSID of the user who replied "recipient": { "id": "987654321098765" },// your account ID "timestamp": 1747231892, "message": { "mid": "aWdtc2c_ZmlkPW...", "text": "This is amazing! Where can I buy it? 😍", // their reply text "reply_to": { // ← presence of this object = it's a story reply "story": { "id": "17893310459840806", // story ID (stable for 30 days) "url": "https://lookaside.fbsbx.com/..." // CDN URL — expires! download now } } } }] }] } // If user replied with an emoji reaction instead of text, you get: // "message": { "mid": "...", "attachments": [{ "type": "like_heart", ... }], "reply_to": {...} } // If user replied with an image/video, attachments array contains the media

Story mention: exact webhook payload

When a user tags your account in their own story, the event arrives as a referral event — completely separate from the message object. The structure is distinctly different, which is why the two are often confused.

Story mention — full webhook payload
{ "object": "instagram", "entry": [{ "id": "987654321098765", "messaging": [{ "sender": { "id": "12345678901234" }, // IGSID of user who mentioned you "recipient": { "id": "987654321098765" }, "timestamp": 1747231892, "referral": { // ← presence of this = story mention or ad entry "source": "STORY_MENTION", // confirms this is a story mention (not an ad) "type": "OPEN_THREAD", "story": { "id": "17893310459898765", // the user's story ID "url": "https://lookaside.fbsbx.com/..." // CDN — download immediately! } }, "message": { // present but often empty for cold-start story mentions "mid": "aWdtc2c..." } }] }] } // KEY DIFFERENCES from story reply: // 1. referral object present, not reply_to // 2. referral.source === "STORY_MENTION" (could also be "ADS" or "CUSTOMER_CHAT_PLUGIN") // 3. Story URL is at referral.story.url, not message.reply_to.story.url // 4. No reply text — user didn't type anything, they just tagged you

The CDN URL expiry problem — download immediately

Both story reply and story mention payloads include a story URL — a CDN link pointing to the story's media (image or video). This URL is temporary. It typically expires within a few hours of the webhook firing. If you store the URL and try to load it later — in your database, in your CRM, in a Slack message — you get a 403 or 404.

The only correct approach: download the story media bytes immediately when the webhook fires, before acknowledging or finishing the event processing.

Node.js — download story media before it expires
downloadStory.js
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const s3 = new S3Client({ region: process.env.AWS_REGION }); async function downloadAndStoreStory(storyUrl, storyId) { // Download story media from CDN — no auth header needed for these URLs const res = await fetch(storyUrl); if (!res.ok) { // URL may have already expired if webhook processing was delayed console.warn(`Story URL expired for story ${storyId}: ${res.status}`); return null; } const contentType = res.headers.get('content-type'); // image/jpeg, video/mp4, etc. const bytes = Buffer.from(await res.arrayBuffer()); const ext = contentType.includes('video') ? 'mp4' : 'jpg'; const key = `stories/${storyId}.${ext}`; // Store permanently in S3 await s3.send(new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, Body: bytes, ContentType: contentType, })); return `s3://${process.env.S3_BUCKET}/${key}`; // save THIS, not the CDN URL }
Python + boto3
download_story.py
import os, boto3, requests s3 = boto3.client("s3", region_name=os.environ["AWS_REGION"]) def download_and_store_story(story_url: str, story_id: str) -> str | None: """Download story media and store permanently. Return S3 path.""" res = requests.get(story_url) if not res.ok: # URL may have already expired print(f"Story URL expired: {res.status_code} for story {story_id}") return None content_type = res.headers.get("content-type", "image/jpeg") ext = "mp4" if "video" in content_type else "jpg" key = f"stories/{story_id}.{ext}" s3.put_object( Bucket=os.environ["S3_BUCKET"], Key=key, Body=res.content, ContentType=content_type, ) return f"s3://{os.environ['S3_BUCKET']}/{key}" # save THIS, not CDN URL

Complete detection and routing code

This is the production-grade handler that correctly identifies both event types from the raw webhook and routes them to appropriate handlers:

Node.js — story event detection and routing
storyRouter.js
async function routeInstagramEvent(event) { const igsid = event.sender.id; // ── Story MENTION (user tagged you in THEIR story) ─────────────────────── if (event.referral?.source === 'STORY_MENTION') { const { story } = event.referral; const storagePath = await downloadAndStoreStory(story.url, story.id); await handleStoryMention({ igsid, storyId: story.id, storagePath }); return; } // ── Story REPLY (user replied to YOUR story via DM) ───────────────────── if (event.message?.reply_to?.story) { const { story } = event.message.reply_to; const replyText = event.message.text || null; // may be null if emoji reaction const attachments = event.message.attachments || []; // for emoji/media replies const storagePath = await downloadAndStoreStory(story.url, story.id); await handleStoryReply({ igsid, storyId: story.id, storagePath, replyText, attachments, mid: event.message.mid, }); return; } // ── Regular DM (no story context) ─────────────────────────────────────── if (event.message?.text) { await handleTextDM({ igsid, text: event.message.text, mid: event.message.mid }); return; } // ── Reaction, read, or other ──────────────────────────────────────────── if (event.reaction) { await handleReaction({ igsid, ...event.reaction }); } }
Python — story event detection and routing
story_router.py
async def route_instagram_event(event: dict) -> None: igsid = event["sender"]["id"] # ── Story MENTION (user tagged you in THEIR story) ─────────────────── referral = event.get("referral", {}) if referral.get("source") == "STORY_MENTION": story = referral["story"] storage_path = download_and_store_story(story["url"], story["id"]) await handle_story_mention(igsid=igsid, story_id=story["id"], storage_path=storage_path) return # ── Story REPLY (user replied to YOUR story via DM) ────────────────── message = event.get("message", {}) reply_to = message.get("reply_to", {}) if "story" in reply_to: story = reply_to["story"] storage_path = download_and_store_story(story["url"], story["id"]) await handle_story_reply( igsid=igsid, story_id=story["id"], storage_path=storage_path, reply_text=message.get("text"), mid=message.get("mid"), ) return # ── Regular DM ──────────────────────────────────────────────────────── if message.get("text"): await handle_text_dm(igsid=igsid, text=message["text"])

Complete auto-response pipeline

Now the useful part — automatically responding to story interactions. The pattern is the same for both event types: detect the story context, compose a contextual reply, and call the Instagram Send API within the 24-hour window.

Node.js — auto-respond to story replies and mentions
storyAutoReply.js
const GRAPH = 'https://graph.facebook.com/v21.0'; const IG_BIZ_ID = process.env.IG_BUSINESS_ID; const TOKEN = process.env.IG_PAGE_TOKEN; async function sendDM(igsid, text) { const res = await fetch(`${GRAPH}/${IG_BIZ_ID}/messages?access_token=${TOKEN}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipient: { id: igsid }, message: { text } }), }); if (!res.ok) console.error('DM send failed', await res.json()); return res.ok; } // Handle story REPLY — user replied to one of your stories async function handleStoryReply({ igsid, storyId, replyText, storagePath }) { // Log for your team console.log(`Story reply from ${igsid}: "${replyText}" to story ${storyId}`); await db.storyReplies.insert({ igsid, storyId, replyText, storagePath }); // Auto-respond within the 24h service window const autoReply = buildStoryReplyResponse(replyText); await sendDM(igsid, autoReply); } // Handle story MENTION — user tagged you in their own story async function handleStoryMention({ igsid, storyId, storagePath }) { console.log(`Story mention from ${igsid}, story ${storyId} saved: ${storagePath}`); await db.storyMentions.insert({ igsid, storyId, storagePath }); // Auto-reply to thank them for the mention await sendDM(igsid, "Hi! 👋 We just saw you mentioned us in your story — thank you so much! " + "We'd love to repost it. Would you be okay with that? Just reply 'Yes' if you agree. 🙏" ); // Optional: alert your team via Slack await notifySlack(`📸 New story mention from ${igsid} | Media: ${storagePath}`); } function buildStoryReplyResponse(replyText) { const text = replyText?.toLowerCase() || ''; // Route to different responses based on intent signals in reply text if (text.includes('buy') || text.includes('price') || text.includes('cost')) { return "Thanks for your interest! 💛 Check our link in bio for pricing. Want me to send you the direct link to this specific product?"; } if (text.includes('where') || text.includes('ship') || text.includes('deliver')) { return "We ship worldwide! 🌍 Orders typically arrive in 5-7 days. Want to place an order?"; } if (text.includes('love') || text.includes('amazing') || text.includes('beautiful')) { return "Aw, thank you so much! 😍 That really makes our day. Check out more on our page!"; } // Default auto-reply for emoji reactions or unmatched text return "Thanks for watching our story! 🙏 Let us know if you have any questions — we're here to help!"; }

The 24-hour window: when you can and cannot reply

Story interactions open a 24-hour messaging window — exactly like regular DMs. When a user replies to your story or mentions you, the window opens immediately and stays open for 24 hours after their last message. Within this window you can send any message freely. After 24 hours, you cannot message that user again unless they send another message.

  • Story reply → window opens immediately. The user initiated contact by replying to your story. You can respond right away and continue the conversation for 24 hours.
  • Story mention → window also opens. The mention is treated as an initiation event. You can DM the person who mentioned you within 24 hours of the referral event firing.
  • Expired window → can't reply. If you process the webhook event more than 24 hours after it fired (delayed processing, retry failure), your reply DM will fail with a Messenger window error.
  • Auto-reply within the webhook handler. The safest approach: send your auto-reply synchronously inside the event handler, immediately after the webhook fires. This guarantees you're within the window.
Check timestamps before replying. If your webhook has retry logic that might re-deliver an old event, always check event.timestamp before sending a reply. If the event is more than 23 hours old, skip the auto-reply — you'd risk sending it outside the window (causing an API error) or sending a duplicate (if the user already received one).

Use cases and response templates

Story reply use cases

  • Purchase intent detection: Keywords like "buy", "price", "where to get" in the reply text trigger a product link or discount code.
  • Support triage: Keywords like "help", "problem", "issue" route the user to your support team via a Slack alert and send a holding message.
  • Engagement nurturing: Emoji reactions or generic positive replies get a warm acknowledgment and a call to action (follow, subscribe, check bio link).
  • Survey or poll: Stories that ask "Would you prefer A or B?" can auto-capture the text reply and log it to a spreadsheet.

Story mention use cases

  • UGC (User Generated Content) collection: Download and store the story media immediately. Ask the user for repost permission. Build a library of authentic customer content.
  • Influencer pipeline: Flag mentions from accounts above a follower threshold for manual review by your marketing team.
  • Thank-you campaign: Auto-send a discount code to anyone who mentions you in their story — rewards organic advocacy.
  • CRM contact creation: First-time mention from a user who isn't in your CRM? Create a contact record with their IGSID.

SocialHook: story events pre-labeled in normalized format

When you connect your Instagram account to SocialHook, story replies and story mentions arrive already classified — you don't parse reply_to.story vs referral.source. The event type field tells you exactly what happened:

Common questions

What is the difference between an Instagram story reply and a story mention?
A story reply: a user replies to one of YOUR stories via DM. Arrives as a messages event with event.message.reply_to.story. Requires the messages webhook subscription. A story mention: a user tags YOUR account in THEIR own story. Arrives as a referral event with event.referral.source === "STORY_MENTION". Requires the messaging_referrals webhook subscription — separate from messages.
Why does the story URL in the webhook expire?
Instagram's story media URLs are hosted on Meta's CDN and are temporary — they typically expire within a few hours. The correct approach: download the story media bytes immediately when the webhook fires and store them in your own permanent storage (S3, GCS, etc.). Save the storage path to your database, not the CDN URL. The story ID (story.id) is stable and can be used as a reference key.
How do I receive Instagram story mentions in my webhook?
You must subscribe to the messaging_referrals webhook field — this is separate from the messages field. Go to: Facebook App → Instagram → Webhooks → Edit Subscriptions → check messaging_referrals. Without this subscription, story mention events silently disappear — no error, no event. If you've had this subscription missing, you cannot retroactively retrieve missed story mentions.
How do I detect if a story reply is a text reply vs an emoji reaction?
Check both event.message.text and event.message.attachments. Text reply: event.message.text contains the string. Emoji/heart reaction as reply: event.message.attachments contains an object with type: "like_heart" or similar sticker type. Media reply (user sent a photo/video as the reply): event.message.attachments with type: "image" or "video" and a payload URL. Both text and attachments also have the reply_to.story context so you know they're story replies.
Can I auto-reply to story mentions within the 24-hour window?
Yes. When a user mentions you in their story, a 24-hour messaging window opens allowing you to DM them. Your webhook receives the referral event — call the Instagram Send API with the user's IGSID as the recipient within 24 hours. This is how you send "thanks for the mention!" messages, repost permission requests, or discount codes automatically. After 24 hours without further interaction, the window closes and you cannot DM them.
What permissions do I need for story reply webhooks?
Same as the Instagram Messaging API generally: instagram_manage_messages, pages_messaging, and pages_read_engagement. For story mentions specifically, you also need the messaging_referrals webhook subscription (this is a subscription field, not an App permission — enable it in your App's webhook settings). All permissions require App Review for production use beyond test users.

story.reply and story.mention
already labeled for you.

SocialHook receives your Instagram story events, downloads the media before the CDN URL expires, classifies story replies vs mentions, and delivers everything as clean normalized JSON. No payload parsing. No CDN race conditions. $50/month for all your Instagram, Messenger, and WhatsApp events.

No credit card required · $50/month after trial · Instagram + Messenger + WhatsApp