Stories disappear after 24 hours, and Instagram has no way to say "tell me when this account posts one" for accounts you do not run. If you follow a brand, a creator or a competitor's public account and want a message the moment a new story appears, a short polling script does the job.
Updated October 2026.
This post builds one in Node.js: poll the stories of a list of accounts, remember which story ids you have already seen, save the media, and send a Telegram message for each new story. It also works out the cost per day, because polling is billed per request and the interval is the number you control.
Code was type-checked against storiesfly 1.1.0 (ESM, Node 18 or newer, no runtime dependencies). If you want the result without writing code, see the last section.
What you need
- A StoriesFly API key. Sign in with Telegram at storiesfly.com/developers/dashboard. A new account gets 100 free requests.
- A Telegram bot token from @BotFather, and the chat id the messages should go to. Send your bot a message, then open
https://api.telegram.org/bot<token>/getUpdatesto see the chat id.
npm install storiesfly
export STORIESFLY_API_KEY="your_key"
export TELEGRAM_BOT_TOKEN="123456:ABC..."
export TELEGRAM_CHAT_ID="123456789"
You never log in to Instagram with your own account. You send an API key, and your account never appears among a story's viewers.
What the stories call returns
import { StoriesFly } from "storiesfly";
const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });
const { user, stories } = await client.user.stories({ username: "instagram" });
console.log(user.id, stories.length);
console.log(stories[0]);
Output shape (values are illustrative):
{
"id": "3012345678901234567",
"url": "https://www.instagram.com/stories/instagram/3012345678901234567/",
"taken_at": 1793000000,
"taken_at_iso": "2026-10-28T09:30:00.000Z",
"media_type": "video",
"media_url": "https://...",
"thumbnail_url": "https://...",
"video_duration": 12.4
}
stories is empty when the account has none up. Ids are strings, which matters: Instagram ids are larger than 2^53 and a JSON number would round them. Always store and compare them as strings.
The polling script
The logic is a diff: fetch the stories that are up now, drop the ids you have already seen, alert on the rest, save the new ids.
import { readFile, writeFile } from "node:fs/promises";
import { StoriesFly, StoriesFlyError, type Story } from "storiesfly";
const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });
const ACCOUNTS = ["instagram", "natgeo"];
const INTERVAL_MS = 30 * 60 * 1000; // every 30 minutes
const SEEN_FILE = "./seen-stories.json";
type Seen = Record<string, string[]>; // username -> story ids
async function loadSeen(): Promise<Seen> {
try {
return JSON.parse(await readFile(SEEN_FILE, "utf8")) as Seen;
} catch {
return {};
}
}
async function notify(username: string, story: Story): Promise<void> {
const text = [
`New ${story.media_type} story from @${username}`,
story.taken_at_iso ?? "",
story.media_url ?? story.url ?? "",
]
.filter(Boolean)
.join("\n");
const res = await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ chat_id: process.env.TELEGRAM_CHAT_ID, text }),
});
if (!res.ok) console.error("Telegram error", res.status, await res.text());
}
async function checkAccount(username: string, seen: Seen): Promise<void> {
const { stories } = await client.user.stories({ username });
const known = new Set(seen[username] ?? []);
const fresh = stories.filter((s) => !known.has(s.id));
// Oldest first, so the chat reads in order.
fresh.sort((a, b) => (a.taken_at ?? 0) - (b.taken_at ?? 0));
for (const story of fresh) await notify(username, story);
// Keep only ids that are still up, so the file does not grow forever.
seen[username] = stories.map((s) => s.id);
}
async function runOnce(): Promise<void> {
const seen = await loadSeen();
for (const username of ACCOUNTS) {
try {
await checkAccount(username, seen);
} catch (err) {
if (err instanceof StoriesFlyError) {
console.error(`@${username}: ${err.code} (request ${err.requestId})`);
if (err.code === "credits_exhausted") return; // stop until you top up
} else {
throw err;
}
}
}
await writeFile(SEEN_FILE, JSON.stringify(seen));
}
await runOnce();
setInterval(() => void runOnce(), INTERVAL_MS);
Notes on the choices in that script:
-
First run. Everything that is up when you start counts as new, so you get one batch of alerts. To avoid that, run once with the
notifycall commented out, then turn it back on. - Why store only ids that are still up. A story lives about 24 hours, so there is no reason to remember an id longer. The file stays small.
-
Errors. A private account answers
private_accountand a missing onenot_found. Both are charged, so remove such usernames from the list rather than polling them forever.rate_limitedis retried by the client twice before it reaches yourcatch. -
One failure does not stop the others. The
tryis inside the loop over accounts. - Order of writes. The seen file is written after the alerts, so a crash between the two can send a duplicate alert. That is the safer direction compared with missing one.
Cron instead of setInterval
A long-running setInterval is fine on a VPS. If you prefer cron, drop the last line (setInterval) and run the script once per tick:
*/30 * * * * cd /path/to/project && node poll.mjs >> poll.log 2>&1
Cron survives restarts and leaves nothing running between ticks. If you compile the script from TypeScript, point cron at the compiled .js file.
Save the media soon
The media_url values are signed links, and they expire within days. A story is gone from the account after a day, and the file link does not last much longer. If you want to keep it, download it when the alert fires:
import { mkdir, writeFile } from "node:fs/promises";
import type { Story } from "storiesfly";
export async function saveStory(username: string, story: Story): Promise<string | null> {
if (!story.media_url) return null;
const res = await fetch(story.media_url);
if (!res.ok) return null;
const ext = story.media_type === "video" ? "mp4" : "jpg";
await mkdir(`./stories/${username}`, { recursive: true });
const path = `./stories/${username}/${story.id}.${ext}`;
await writeFile(path, Buffer.from(await res.arrayBuffer()));
return path;
}
Call saveStory(username, story) next to notify. If you would rather get the file in Telegram than a link, the Bot API has sendVideo and sendPhoto, which take a URL.
What it costs
Each client.user.stories call is one request, and requests start from $1.50 per 1,000. The cost is accounts x polls per day:
| Accounts | Interval | Requests / day | Cost / day | Cost / 30 days |
|---|---|---|---|---|
| 5 | 60 min | 120 | $0.18 | $5.40 |
| 20 | 30 min | 960 | $1.44 | $43.20 |
| 20 | 15 min | 1,920 | $2.88 | $86.40 |
| 100 | 60 min | 2,400 | $3.60 | $108 |
The interval is the lever. Stories stay up for a day, so polling every 30 to 60 minutes still catches each one with hours to spare. Polling every minute only buys you earlier alerts, not more of them. A good rule is to poll busy accounts often and quiet ones rarely: keep two lists with two intervals.
Two things cost nothing to check. Your balance (client.account() is free) and the per-call charge (pass { withMeta: true } and read meta.unitsCharged). Add a guard that stops the script when balanceRemaining falls below a number you pick. It comes as a string in US dollars, so compare Number(meta.balanceRemaining).
Privacy, in one paragraph
You never log in with your own account, so your account never appears among a story's viewers. The person is not notified. The script reads public stories only: a private account gives private_account, and you cannot get around that.
Limits to know about
- It only sees stories that are up now. There is no history, so if the script was down for two days, you missed those stories.
- It detects a story at the next poll, not at the moment it is posted. The delay is at most your interval.
- Highlights are a separate call (
client.user.highlights), not part of this script.
If you do not want to run this yourself
The script above is a few dozen lines, but you have to host it, keep the bot token safe and watch the balance. If you only want the alerts, StoriesFly has a Telegram bot and a tracker that does this without code: you add an account and get a message when it posts. The API is the better fit if you want the data inside your own app.
FAQ
How do I get the Instagram story API without logging in?
Use an API key from the dashboard and call client.user.stories({ username }). There is no Instagram login, cookie or session on your side.
Can I look up an account by id?
Yes. client.user.stories({ id }) takes an id instead of a username. Pass one of the two, never both.
How often should I poll?
Every 30 to 60 minutes is enough for most uses, since a story stays up for a day.
Will the account know I looked?
The person is not notified.
Do the media links last?
No. They expire within days, so save the files when you get the alert.
Full method list and response types: storiesfly.com/developers/docs.
Top comments (0)