@storylinkpro/instagram-stories-api 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ - The package description, keywords and README make the package easier to find.
6
+ - A GitHub Actions workflow publishes new versions with npm provenance.
7
+
8
+ ## 0.1.0
9
+
10
+ - First release. A typed client and CLI that read an Instagram account, its Stories, posts and reels, and their insights.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Storylink
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,149 @@
1
- # Temporary Holding Version
1
+ # @storylinkpro/instagram-stories-api
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@storylinkpro/instagram-stories-api)](https://www.npmjs.com/package/@storylinkpro/instagram-stories-api)
4
+ [![CI](https://github.com/storylinkpro/instagram-stories-api/actions/workflows/ci.yml/badge.svg)](https://github.com/storylinkpro/instagram-stories-api/actions/workflows/ci.yml)
5
+ [![types](https://img.shields.io/npm/types/@storylinkpro/instagram-stories-api)](https://www.npmjs.com/package/@storylinkpro/instagram-stories-api)
6
+ [![license](https://img.shields.io/npm/l/@storylinkpro/instagram-stories-api)](LICENSE)
7
+
8
+ **An Instagram Graph API client and CLI for Node.js and TypeScript.** It reads Instagram Stories, posts and reels. It also reads their insights: views, reach, replies, likes, comments, saves and shares. It has no dependencies. It needs Node 20+ or another runtime with `fetch`.
9
+
10
+ Use it in a script, a cron job or your own Instagram analytics dashboard.
11
+
12
+ > Unofficial. Meta did not make this package and does not support it. It calls Meta's public Graph API at `graph.facebook.com`.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install @storylinkpro/instagram-stories-api
18
+ ```
19
+
20
+ ## Quick start: read Instagram story insights
21
+
22
+ ```ts
23
+ import { createClient } from "@storylinkpro/instagram-stories-api";
24
+
25
+ const ig = createClient({
26
+ accessToken: process.env.INSTAGRAM_ACCESS_TOKEN!,
27
+ igUserId: process.env.INSTAGRAM_USER_ID, // the Instagram professional account id
28
+ });
29
+
30
+ const { username, followers_count } = await ig.account();
31
+
32
+ for (const story of await ig.stories()) {
33
+ const { views, reach, replies } = await ig.storyInsights(story.id);
34
+ console.log(story.id, views, reach, replies);
35
+ }
36
+ ```
37
+
38
+ ## Command line
39
+
40
+ You can query the API from a terminal without an install:
41
+
42
+ ```sh
43
+ npx @storylinkpro/instagram-stories-api stories
44
+ ```
45
+
46
+ After `npm install -g @storylinkpro/instagram-stories-api`, the command is `instagram-stories-api`.
47
+
48
+ ```text
49
+ ID TYPE POSTED (UTC) PERMALINK
50
+ 18012345678901234 IMAGE 2026-11-07 08:30 https://www.instagram.com/stories/example/18012345678901234
51
+ 18012345678905678 VIDEO 2026-11-07 09:10 https://www.instagram.com/stories/example/18012345678905678
52
+ ```
53
+
54
+ | Command | Prints |
55
+ |---|---|
56
+ | `account` | The account's id, username, name, followers, follows and media count. |
57
+ | `stories` | The account's current stories. Instagram keeps a story for 24 hours. |
58
+ | `media [--limit n]` | The most recent posts and reels. The default is 10. |
59
+ | `insights <media-id> [--type story\|post\|reel]` | Insights for one media. The default type is `story`. |
60
+ | `account-insights [--since 7d]` | Account totals for a window: views, reach, likes, comments, shares and replies. |
61
+
62
+ Options:
63
+
64
+ - `--token`: the access token. The default is `INSTAGRAM_ACCESS_TOKEN`.
65
+ - `--account`, `-a`: the Instagram account id. The default is `INSTAGRAM_USER_ID`.
66
+ - `--since`, `-s` and `--until`, `-u`: `24h`, `7d`, `30d` or a date such as `2026-10-01`. Meta rejects a window of 30 days or more.
67
+ - `--metrics`: account metrics, separated by commas.
68
+ - `--api-version`: the Graph API version. The default is `v25.0`.
69
+ - `--json`: print JSON for a script or another tool.
70
+ - `--help`, `-h`: help for the CLI or for one command.
71
+
72
+ The CLI never prints the token. The exit code is 0 on success, 1 when Meta returns an error, and 2 for a usage error.
73
+
74
+ ## Methods
75
+
76
+ | Method | Returns |
77
+ |---|---|
78
+ | `account()` | `{ id, username, name, followers_count, follows_count, media_count, profile_picture_url }`. |
79
+ | `stories()` | The current stories as `Media[]`. It reads every page. |
80
+ | `media({ limit?, since?, until?, after? })` | One page of posts and reels: `{ data, after, hasNext }`. Pass `after` to get the next page. |
81
+ | `iterateMedia({ ..., max? })` | An async iterator over all posts and reels. It follows `paging.next`. `max` stops it early. |
82
+ | `storyInsights(mediaId)` | `{ views, reach, replies, shares }`. |
83
+ | `postInsights(mediaId)` | `{ views, reach, likes, comments, saves, shares }`. |
84
+ | `reelInsights(mediaId)` | `{ views, reach, likes, comments, saves, shares }`. |
85
+ | `accountInsights({ metrics?, since?, until?, period?, metricType?, breakdown?, timeframe? })` | One `{ name, period, total, values, breakdowns }` per metric. |
86
+
87
+ A `Media` has `id`, `media_type`, `media_product_type`, `timestamp`, `permalink`, `caption`, `media_url` and `thumbnail_url`. A field that Meta leaves out is `null`.
88
+
89
+ An insight that Meta does not return is `null`, not `0`. Meta withholds some story numbers when fewer than 5 people saw the story. Meta calls saves `saved`. The client returns them as `saves`.
90
+
91
+ `accountInsights()` sends `period=day` and `metric_type=total_value` by default. For follower demographics, pass `metrics: ["follower_demographics"]`, `period: "lifetime"`, `timeframe: "this_month"` and a `breakdown` such as `"country"` or `"age,gender"`.
92
+
93
+ `since` and `until` take a `Date`, epoch milliseconds or a date string. The client sends Unix seconds.
94
+
95
+ Methods that read an account take `igUserId` to use another account than the client's. Every method takes `signal` (an `AbortSignal`).
96
+
97
+ `createClient()` also takes `apiVersion` (default `v25.0`), `fetch` (for tests or another runtime) and `timeoutMs` (default 30000).
98
+
99
+ ## Errors
100
+
101
+ A failed call throws `InstagramApiError`. It has `status`, and Meta's `code`, `subcode`, `type`, `fbtraceId` and `message`. It also has these flags:
102
+
103
+ - `isAuthError`: code 190, or subcode 458, 460, 463 or 467. The token expired, someone revoked it, or it is not valid. Get a new token.
104
+ - `isRateLimit`: code 4, 17, 32, 613, 80001 or 80002. Meta throttled the app or the account. Wait before you try again.
105
+
106
+ A network error or a timeout has `status` 0. The message never contains the access token.
107
+
108
+ ```ts
109
+ import { InstagramApiError } from "@storylinkpro/instagram-stories-api";
110
+
111
+ try {
112
+ await ig.stories();
113
+ } catch (e) {
114
+ if (e instanceof InstagramApiError && e.isAuthError) console.error("Get a new token");
115
+ else throw e;
116
+ }
117
+ ```
118
+
119
+ ### Instagram API rate limits
120
+
121
+ Meta sends usage headers on each response. `ig.lastUsage` holds the most recent ones, and `error.usage` holds the ones of a failed call:
122
+
123
+ ```ts
124
+ ig.lastUsage?.app; // { call_count, total_time, total_cputime } from x-app-usage, in percent
125
+ ig.lastUsage?.businessUseCase; // from x-business-use-case-usage, per business object id
126
+ ```
127
+
128
+ The client does not retry. You decide when to call again.
129
+
130
+ ## Get an Instagram Graph API access token
131
+
132
+ This version uses the Instagram API with Facebook Login. You need:
133
+
134
+ 1. An Instagram professional account (Business or Creator) that is linked to a Facebook Page.
135
+ 2. A Meta app with the `instagram_basic`, `instagram_manage_insights` and `pages_read_engagement` permissions.
136
+ 3. A Facebook User or Page access token. For a quick test, get one in the [Graph API Explorer](https://developers.facebook.com/tools/explorer/).
137
+
138
+ To find the Instagram account id, call `GET /me/accounts?fields=instagram_business_account` with the token. See Meta's [Instagram Platform docs](https://developers.facebook.com/docs/instagram-platform) for the full steps.
139
+
140
+ Keep the token on the server. Do not send it to the browser.
141
+
142
+ ## Related
143
+
144
+ - [Storylink](https://storylink.pro) schedules Instagram Stories, posts and reels, and shows their results. Storylink made this package.
145
+ - [`storylink-mcp`](https://www.npmjs.com/package/storylink-mcp) connects Claude and other AI assistants to your Storylink Instagram analytics.
146
+
147
+ ## License
148
+
149
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./cli.js";
3
+ const code = await run(process.argv.slice(2), {
4
+ env: process.env,
5
+ stdout: (s) => process.stdout.write(s),
6
+ stderr: (s) => process.stderr.write(s),
7
+ });
8
+ process.exitCode = code;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export declare const EXIT_OK = 0;
2
+ export declare const EXIT_API_ERROR = 1;
3
+ export declare const EXIT_USAGE = 2;
4
+ export type CliIo = {
5
+ env: Record<string, string | undefined>;
6
+ stdout: (text: string) => void;
7
+ stderr: (text: string) => void;
8
+ fetch?: typeof fetch;
9
+ now?: () => Date;
10
+ };
11
+ /** Parse `7d`, `24h` or a date string into a Date. */
12
+ export declare function parseWhen(value: string, now: Date): Date;
13
+ /** Render rows as a plain text table. Number columns are right-aligned. */
14
+ export declare function table(headers: string[], rows: Array<Array<string | number | null>>, numeric?: boolean[]): string;
15
+ export declare function run(argv: string[], io: CliIo): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,246 @@
1
+ import { parseArgs } from "node:util";
2
+ import { createClient } from "./client.js";
3
+ import { InstagramApiError, redact } from "./errors.js";
4
+ export const EXIT_OK = 0;
5
+ export const EXIT_API_ERROR = 1;
6
+ export const EXIT_USAGE = 2;
7
+ class UsageError extends Error {
8
+ }
9
+ const COMMANDS = ["account", "stories", "media", "insights", "account-insights"];
10
+ const GLOBAL_HELP = `Usage: instagram-stories-api <command> [options]
11
+
12
+ Read Instagram Stories, media and insights from the Instagram Graph API.
13
+
14
+ Commands:
15
+ account Show the account's profile and counts.
16
+ stories List the account's current stories (last 24 hours).
17
+ media [--limit n] List recent posts and reels. Default 10.
18
+ insights <media-id> Show insights for one media. Add --type story|post|reel.
19
+ account-insights Show account insights. Add --since 7d|30d|YYYY-MM-DD.
20
+
21
+ Options:
22
+ --token <token> Access token. Default: $INSTAGRAM_ACCESS_TOKEN.
23
+ --account, -a <id> Instagram account id. Default: $INSTAGRAM_USER_ID.
24
+ --api-version <v> Graph API version. Default: v25.0.
25
+ --json Print JSON.
26
+ --help, -h Show help. "<command> --help" shows help for one command.
27
+
28
+ Exit codes: 0 success, 1 API error, 2 usage error.
29
+ Unofficial. Not made or supported by Meta. Made by Storylink: https://storylink.pro`;
30
+ const COMMAND_HELP = {
31
+ account: `Usage: instagram-stories-api account [--account <id>] [--json]
32
+
33
+ Show the account's id, username, name, followers, follows and media count.`,
34
+ stories: `Usage: instagram-stories-api stories [--account <id>] [--json]
35
+
36
+ List the account's current stories. Instagram keeps a story for 24 hours.`,
37
+ media: `Usage: instagram-stories-api media [--limit n] [--account <id>] [--json]
38
+
39
+ List the account's most recent posts and reels, newest first.
40
+
41
+ --limit, -n <n> The number of items. Default 10.`,
42
+ insights: `Usage: instagram-stories-api insights <media-id> [--type story|post|reel] [--json]
43
+
44
+ Show insights for one media.
45
+
46
+ --type, -t <type> story (default): views, reach, replies, shares.
47
+ post or reel: views, reach, likes, comments, saves, shares.`,
48
+ "account-insights": `Usage: instagram-stories-api account-insights [--since 7d] [--until <date>] [--metrics a,b] [--json]
49
+
50
+ Show account totals for a time window.
51
+
52
+ --since, -s <when> 24h, 7d, 30d or a date such as 2026-10-01. Default 7d.
53
+ --until, -u <when> The end of the window. Default now.
54
+ --metrics <list> Comma-separated metrics. Default views,reach,likes,comments,shares,replies.
55
+
56
+ Meta rejects a window of 30 days or more.`,
57
+ };
58
+ /** Parse `7d`, `24h` or a date string into a Date. */
59
+ export function parseWhen(value, now) {
60
+ const rel = /^(\d+)([dh])$/.exec(value);
61
+ if (rel) {
62
+ const n = Number(rel[1]);
63
+ const ms = rel[2] === "d" ? n * 86_400_000 : n * 3_600_000;
64
+ return new Date(now.getTime() - ms);
65
+ }
66
+ if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
67
+ const t = Date.parse(value);
68
+ if (Number.isFinite(t))
69
+ return new Date(t);
70
+ }
71
+ throw new UsageError(`Not a valid time: "${value}". Use 24h, 7d, 30d or YYYY-MM-DD.`);
72
+ }
73
+ const fmtNum = (n) => typeof n === "number" ? n.toLocaleString("en-US") : "-";
74
+ /** Render rows as a plain text table. Number columns are right-aligned. */
75
+ export function table(headers, rows, numeric = []) {
76
+ const cells = rows.map((r) => r.map((c) => (typeof c === "number" ? fmtNum(c) : c ?? "-")));
77
+ const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((r) => (r[i] ?? "").length)));
78
+ const line = (r) => r
79
+ .map((c, i) => (numeric[i] ? c.padStart(widths[i]) : c.padEnd(widths[i])))
80
+ .join(" ")
81
+ .trimEnd();
82
+ return [line(headers), ...cells.map(line)].join("\n");
83
+ }
84
+ function shortTime(iso) {
85
+ return iso ? iso.replace("T", " ").replace(/:\d{2}(\+0000|Z)$/, "").slice(0, 16) : "-";
86
+ }
87
+ function oneLine(text, max = 40) {
88
+ if (!text)
89
+ return "-";
90
+ const flat = text.replace(/\s+/g, " ").trim();
91
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
92
+ }
93
+ export async function run(argv, io) {
94
+ let token;
95
+ const out = (s) => io.stdout(redact(s, token) + "\n");
96
+ const err = (s) => io.stderr(redact(s, token) + "\n");
97
+ try {
98
+ let parsed;
99
+ try {
100
+ parsed = parseArgs({
101
+ args: argv,
102
+ allowPositionals: true,
103
+ strict: true,
104
+ options: {
105
+ token: { type: "string" },
106
+ account: { type: "string", short: "a" },
107
+ "api-version": { type: "string" },
108
+ json: { type: "boolean" },
109
+ help: { type: "boolean", short: "h" },
110
+ limit: { type: "string", short: "n" },
111
+ type: { type: "string", short: "t" },
112
+ since: { type: "string", short: "s" },
113
+ until: { type: "string", short: "u" },
114
+ metrics: { type: "string" },
115
+ },
116
+ });
117
+ }
118
+ catch (e) {
119
+ throw new UsageError(e instanceof Error ? e.message : String(e));
120
+ }
121
+ const { values, positionals } = parsed;
122
+ token = values.token ?? io.env.INSTAGRAM_ACCESS_TOKEN;
123
+ const [cmdName, ...rest] = positionals;
124
+ if (!cmdName) {
125
+ (values.help ? out : err)(GLOBAL_HELP);
126
+ return values.help ? EXIT_OK : EXIT_USAGE;
127
+ }
128
+ if (!COMMANDS.includes(cmdName)) {
129
+ throw new UsageError(`Unknown command "${cmdName}". Run "instagram-stories-api --help".`);
130
+ }
131
+ const cmd = cmdName;
132
+ if (values.help) {
133
+ out(COMMAND_HELP[cmd]);
134
+ return EXIT_OK;
135
+ }
136
+ const maxPositionals = cmd === "insights" ? 1 : 0;
137
+ if (rest.length > maxPositionals)
138
+ throw new UsageError(`Unexpected argument "${rest[maxPositionals]}".`);
139
+ if (!token)
140
+ throw new UsageError("No access token. Pass --token or set INSTAGRAM_ACCESS_TOKEN.");
141
+ const igUserId = values.account ?? io.env.INSTAGRAM_USER_ID;
142
+ if (cmd !== "insights" && !igUserId) {
143
+ throw new UsageError("No account id. Pass --account or set INSTAGRAM_USER_ID.");
144
+ }
145
+ let client;
146
+ try {
147
+ client = createClient({
148
+ accessToken: token,
149
+ igUserId,
150
+ apiVersion: values["api-version"],
151
+ fetch: io.fetch,
152
+ });
153
+ }
154
+ catch (e) {
155
+ throw new UsageError(e instanceof Error ? e.message : String(e));
156
+ }
157
+ const json = values.json === true;
158
+ const print = (data, text) => out(json ? JSON.stringify(data, null, 2) : text());
159
+ const now = io.now?.() ?? new Date();
160
+ switch (cmd) {
161
+ case "account": {
162
+ const a = await client.account();
163
+ print(a, () => table(["FIELD", "VALUE"], [
164
+ ["id", a.id],
165
+ ["username", a.username],
166
+ ["name", a.name],
167
+ ["followers", fmtNum(a.followers_count)],
168
+ ["follows", fmtNum(a.follows_count)],
169
+ ["media", fmtNum(a.media_count)],
170
+ ]));
171
+ return EXIT_OK;
172
+ }
173
+ case "stories": {
174
+ const s = await client.stories();
175
+ print(s, () => s.length === 0
176
+ ? "No current stories."
177
+ : table(["ID", "TYPE", "POSTED (UTC)", "PERMALINK"], s.map((m) => [m.id, m.media_type, shortTime(m.timestamp), m.permalink])));
178
+ return EXIT_OK;
179
+ }
180
+ case "media": {
181
+ const limit = values.limit === undefined ? 10 : Number(values.limit);
182
+ if (!Number.isInteger(limit) || limit < 1)
183
+ throw new UsageError("--limit must be a positive whole number.");
184
+ const items = [];
185
+ for await (const m of client.iterateMedia({ max: limit, limit: Math.min(limit, 50) }))
186
+ items.push(m);
187
+ print(items, () => items.length === 0
188
+ ? "No media."
189
+ : table(["ID", "KIND", "POSTED (UTC)", "CAPTION"], items.map((m) => [m.id, m.media_product_type ?? m.media_type, shortTime(m.timestamp), oneLine(m.caption)])));
190
+ return EXIT_OK;
191
+ }
192
+ case "insights": {
193
+ const mediaId = rest[0];
194
+ if (!mediaId)
195
+ throw new UsageError("Missing <media-id>. Usage: instagram-stories-api insights <media-id>");
196
+ if (!/^[A-Za-z0-9_]+$/.test(mediaId))
197
+ throw new UsageError("<media-id> must contain only letters, digits or \"_\".");
198
+ const type = values.type ?? "story";
199
+ let result;
200
+ if (type === "story")
201
+ result = await client.storyInsights(mediaId);
202
+ else if (type === "post")
203
+ result = await client.postInsights(mediaId);
204
+ else if (type === "reel")
205
+ result = await client.reelInsights(mediaId);
206
+ else
207
+ throw new UsageError(`--type must be story, post or reel, not "${type}".`);
208
+ print(result, () => table(["METRIC", "VALUE"], Object.entries(result).map(([k, v]) => [k, v]), [false, true]));
209
+ return EXIT_OK;
210
+ }
211
+ case "account-insights": {
212
+ const since = parseWhen(values.since ?? "7d", now);
213
+ const until = values.until ? parseWhen(values.until, now) : now;
214
+ if (since >= until)
215
+ throw new UsageError("--since must be before --until.");
216
+ const metrics = values.metrics
217
+ ?.split(",")
218
+ .map((m) => m.trim())
219
+ .filter(Boolean);
220
+ if (metrics && metrics.length === 0)
221
+ throw new UsageError("--metrics must name at least one metric.");
222
+ const rows = await client.accountInsights({ since, until, metrics });
223
+ print(rows, () => table(["METRIC", "TOTAL"], rows.map((r) => [r.name, r.total]), [false, true]));
224
+ return EXIT_OK;
225
+ }
226
+ }
227
+ }
228
+ catch (e) {
229
+ if (e instanceof UsageError || e instanceof TypeError) {
230
+ err(`Error: ${e.message}`);
231
+ return EXIT_USAGE;
232
+ }
233
+ if (e instanceof InstagramApiError) {
234
+ err(`Error: ${e.message}`);
235
+ if (e.isAuthError)
236
+ err("The access token is expired or not valid. Get a new token.");
237
+ else if (e.isRateLimit)
238
+ err("Meta rate-limited this app or account. Wait and try again.");
239
+ if (e.fbtraceId)
240
+ err(`fbtrace_id: ${e.fbtraceId}`);
241
+ return EXIT_API_ERROR;
242
+ }
243
+ err(`Error: ${e instanceof Error ? e.message : String(e)}`);
244
+ return EXIT_API_ERROR;
245
+ }
246
+ }
@@ -0,0 +1,144 @@
1
+ import { type TimeInput } from "./time.js";
2
+ import { type RateLimitUsage } from "./usage.js";
3
+ export declare const DEFAULT_API_VERSION = "v25.0";
4
+ export declare const GRAPH_HOST = "https://graph.facebook.com";
5
+ /** Fields read for every media object (stories, posts and reels). */
6
+ export declare const MEDIA_FIELDS: readonly ["id", "media_type", "media_product_type", "timestamp", "permalink", "caption", "media_url", "thumbnail_url"];
7
+ /** Fields read by `account()`. */
8
+ export declare const ACCOUNT_FIELDS: readonly ["id", "username", "name", "followers_count", "follows_count", "media_count", "profile_picture_url"];
9
+ /** Metrics requested by `storyInsights()`. */
10
+ export declare const STORY_METRICS: readonly ["views", "reach", "replies", "shares"];
11
+ /** Metrics requested by `postInsights()`. Meta calls saves `saved`. */
12
+ export declare const POST_METRICS: readonly ["views", "reach", "likes", "comments", "saved", "shares"];
13
+ /** Metrics requested by `reelInsights()`. Meta calls saves `saved`. */
14
+ export declare const REEL_METRICS: readonly ["views", "reach", "likes", "comments", "saved", "shares"];
15
+ /** Default metrics for `accountInsights()`. */
16
+ export declare const DEFAULT_ACCOUNT_METRICS: readonly ["views", "reach", "likes", "comments", "shares", "replies"];
17
+ export type ClientOptions = {
18
+ /** A Facebook User or Page access token with Instagram permissions. */
19
+ accessToken: string;
20
+ /** The Instagram professional account id. Methods can also take it per call. */
21
+ igUserId?: string;
22
+ /** Graph API version. Default `v25.0`. */
23
+ apiVersion?: string;
24
+ /** A `fetch` implementation. Default: the global `fetch`. */
25
+ fetch?: typeof fetch;
26
+ /** Abort a request after this many milliseconds. Default 30000. */
27
+ timeoutMs?: number;
28
+ };
29
+ export type CallOptions = {
30
+ /** Use this account instead of the client's `igUserId`. */
31
+ igUserId?: string;
32
+ signal?: AbortSignal;
33
+ };
34
+ export type Account = {
35
+ id: string;
36
+ username: string | null;
37
+ name: string | null;
38
+ followers_count: number | null;
39
+ follows_count: number | null;
40
+ media_count: number | null;
41
+ profile_picture_url: string | null;
42
+ };
43
+ export type MediaType = "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM" | (string & {});
44
+ export type MediaProductType = "FEED" | "REELS" | "STORY" | "AD" | (string & {});
45
+ export type Media = {
46
+ id: string;
47
+ media_type: MediaType | null;
48
+ media_product_type: MediaProductType | null;
49
+ /** ISO 8601 time the media was published. */
50
+ timestamp: string | null;
51
+ permalink: string | null;
52
+ caption: string | null;
53
+ /** Meta leaves this out for some videos (for example, ones with licensed audio). */
54
+ media_url: string | null;
55
+ thumbnail_url: string | null;
56
+ };
57
+ export type MediaOptions = CallOptions & {
58
+ /** Items per page. Meta's default is 25. */
59
+ limit?: number;
60
+ since?: TimeInput;
61
+ until?: TimeInput;
62
+ /** A cursor from a previous page's `after`. */
63
+ after?: string;
64
+ };
65
+ export type MediaPage = {
66
+ data: Media[];
67
+ /** Cursor for the next page, or null on the last page. */
68
+ after: string | null;
69
+ /** True when Meta says there is another page. */
70
+ hasNext: boolean;
71
+ };
72
+ export type StoryInsights = {
73
+ views: number | null;
74
+ reach: number | null;
75
+ replies: number | null;
76
+ shares: number | null;
77
+ };
78
+ export type PostInsights = {
79
+ views: number | null;
80
+ reach: number | null;
81
+ likes: number | null;
82
+ comments: number | null;
83
+ saves: number | null;
84
+ shares: number | null;
85
+ };
86
+ export type ReelInsights = PostInsights;
87
+ export type AccountInsightsOptions = CallOptions & {
88
+ /** Metric names. Default: views, reach, likes, comments, shares, replies. */
89
+ metrics?: readonly string[];
90
+ /** Default `day`. Demographic metrics use `lifetime`. */
91
+ period?: string;
92
+ /** Default `total_value`. Pass `null` to leave it out. */
93
+ metricType?: "total_value" | "time_series" | null;
94
+ since?: TimeInput;
95
+ until?: TimeInput;
96
+ /** For example `media_product_type`, or `age,gender` for demographics. */
97
+ breakdown?: string;
98
+ /** For demographic metrics, for example `this_month`. */
99
+ timeframe?: string;
100
+ };
101
+ export type AccountInsightBreakdown = {
102
+ dimensionKeys: string[];
103
+ results: Array<{
104
+ dimensionValues: string[];
105
+ value: number;
106
+ }>;
107
+ };
108
+ export type AccountInsight = {
109
+ name: string;
110
+ period: string | null;
111
+ title: string | null;
112
+ description: string | null;
113
+ /** `total_value.value` when the metric has one, otherwise null. */
114
+ total: number | null;
115
+ /** Values per period, for `time_series` metrics. */
116
+ values: Array<{
117
+ value: unknown;
118
+ endTime: string | null;
119
+ }>;
120
+ breakdowns: AccountInsightBreakdown[];
121
+ };
122
+ export type InstagramClient = {
123
+ readonly apiVersion: string;
124
+ readonly baseUrl: string;
125
+ /** Usage headers from the most recent response, or null before the first call. */
126
+ readonly lastUsage: RateLimitUsage | null;
127
+ account(options?: CallOptions): Promise<Account>;
128
+ stories(options?: CallOptions): Promise<Media[]>;
129
+ media(options?: MediaOptions): Promise<MediaPage>;
130
+ iterateMedia(options?: MediaOptions & {
131
+ max?: number;
132
+ }): AsyncGenerator<Media, void, undefined>;
133
+ storyInsights(mediaId: string, options?: {
134
+ signal?: AbortSignal;
135
+ }): Promise<StoryInsights>;
136
+ postInsights(mediaId: string, options?: {
137
+ signal?: AbortSignal;
138
+ }): Promise<PostInsights>;
139
+ reelInsights(mediaId: string, options?: {
140
+ signal?: AbortSignal;
141
+ }): Promise<ReelInsights>;
142
+ accountInsights(options?: AccountInsightsOptions): Promise<AccountInsight[]>;
143
+ };
144
+ export declare function createClient(options: ClientOptions): InstagramClient;
package/dist/client.js ADDED
@@ -0,0 +1,300 @@
1
+ import { InstagramApiError, redact } from "./errors.js";
2
+ import { toUnixSeconds } from "./time.js";
3
+ import { parseUsageHeaders } from "./usage.js";
4
+ export const DEFAULT_API_VERSION = "v25.0";
5
+ export const GRAPH_HOST = "https://graph.facebook.com";
6
+ const DEFAULT_TIMEOUT_MS = 30_000;
7
+ /** Fields read for every media object (stories, posts and reels). */
8
+ export const MEDIA_FIELDS = [
9
+ "id",
10
+ "media_type",
11
+ "media_product_type",
12
+ "timestamp",
13
+ "permalink",
14
+ "caption",
15
+ "media_url",
16
+ "thumbnail_url",
17
+ ];
18
+ /** Fields read by `account()`. */
19
+ export const ACCOUNT_FIELDS = [
20
+ "id",
21
+ "username",
22
+ "name",
23
+ "followers_count",
24
+ "follows_count",
25
+ "media_count",
26
+ "profile_picture_url",
27
+ ];
28
+ /** Metrics requested by `storyInsights()`. */
29
+ export const STORY_METRICS = ["views", "reach", "replies", "shares"];
30
+ /** Metrics requested by `postInsights()`. Meta calls saves `saved`. */
31
+ export const POST_METRICS = ["views", "reach", "likes", "comments", "saved", "shares"];
32
+ /** Metrics requested by `reelInsights()`. Meta calls saves `saved`. */
33
+ export const REEL_METRICS = ["views", "reach", "likes", "comments", "saved", "shares"];
34
+ /** Default metrics for `accountInsights()`. */
35
+ export const DEFAULT_ACCOUNT_METRICS = ["views", "reach", "likes", "comments", "shares", "replies"];
36
+ const ID_PATTERN = /^[A-Za-z0-9_]+$/;
37
+ function checkId(value, what) {
38
+ if (!value)
39
+ throw new TypeError(`${what} is required`);
40
+ if (!ID_PATTERN.test(value))
41
+ throw new TypeError(`${what} must contain only letters, digits or "_"`);
42
+ return value;
43
+ }
44
+ const str = (v) => (typeof v === "string" ? v : null);
45
+ const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
46
+ function toMedia(raw) {
47
+ return {
48
+ id: String(raw.id ?? ""),
49
+ media_type: str(raw.media_type),
50
+ media_product_type: str(raw.media_product_type),
51
+ timestamp: str(raw.timestamp),
52
+ permalink: str(raw.permalink),
53
+ caption: str(raw.caption),
54
+ media_url: str(raw.media_url),
55
+ thumbnail_url: str(raw.thumbnail_url),
56
+ };
57
+ }
58
+ /** Read one number per metric name. A missing metric stays out of the map. */
59
+ function metricValues(body) {
60
+ const out = new Map();
61
+ for (const row of body.data ?? []) {
62
+ if (!row.name)
63
+ continue;
64
+ const value = num(row.values?.[0]?.value) ?? num(row.total_value?.value);
65
+ if (value !== null)
66
+ out.set(row.name, value);
67
+ }
68
+ return out;
69
+ }
70
+ function combineSignals(a, b) {
71
+ if (!a)
72
+ return b;
73
+ const any = AbortSignal.any;
74
+ return any ? any([a, b]) : a;
75
+ }
76
+ export function createClient(options) {
77
+ if (!options || typeof options.accessToken !== "string" || options.accessToken.length === 0) {
78
+ throw new TypeError("accessToken is required");
79
+ }
80
+ const token = options.accessToken;
81
+ const apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
82
+ if (!/^v\d+\.\d+$/.test(apiVersion)) {
83
+ throw new TypeError(`apiVersion must look like "v25.0", got "${apiVersion}"`);
84
+ }
85
+ const baseUrl = `${GRAPH_HOST}/${apiVersion}`;
86
+ const doFetch = options.fetch ?? globalThis.fetch;
87
+ if (typeof doFetch !== "function") {
88
+ throw new TypeError("No fetch available. Use Node 20+ or pass options.fetch");
89
+ }
90
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
91
+ let lastUsage = null;
92
+ const userId = (o) => checkId(o?.igUserId ?? options.igUserId, "igUserId");
93
+ async function call(url, signal) {
94
+ url.searchParams.set("access_token", token);
95
+ const path = url.pathname;
96
+ let res;
97
+ try {
98
+ res = await doFetch(url.toString(), {
99
+ method: "GET",
100
+ headers: { accept: "application/json" },
101
+ signal: combineSignals(signal, AbortSignal.timeout(timeoutMs)),
102
+ });
103
+ }
104
+ catch (e) {
105
+ const isTimeout = e instanceof Error && e.name === "TimeoutError";
106
+ const reason = isTimeout
107
+ ? `timed out after ${timeoutMs} ms`
108
+ : e instanceof Error
109
+ ? e.message
110
+ : String(e);
111
+ throw new InstagramApiError({
112
+ message: redact(`Request to ${path} failed: ${reason}`, token),
113
+ status: 0,
114
+ type: isTimeout ? "Timeout" : "NetworkError",
115
+ path,
116
+ });
117
+ }
118
+ const usage = parseUsageHeaders(res.headers);
119
+ lastUsage = usage;
120
+ const text = await res.text();
121
+ let body = null;
122
+ try {
123
+ body = text ? JSON.parse(text) : null;
124
+ }
125
+ catch {
126
+ body = null;
127
+ }
128
+ const err = body?.error;
129
+ if (!res.ok || err) {
130
+ const detail = err?.message ?? `HTTP ${res.status}`;
131
+ const codes = [
132
+ err?.code !== undefined ? `code ${err.code}` : null,
133
+ err?.error_subcode !== undefined ? `subcode ${err.error_subcode}` : null,
134
+ ].filter(Boolean);
135
+ throw new InstagramApiError({
136
+ message: redact(`Instagram API error at ${path}: ${detail}${codes.length ? ` (${codes.join(", ")})` : ""}`, token),
137
+ status: res.status,
138
+ code: num(err?.code),
139
+ subcode: num(err?.error_subcode),
140
+ type: str(err?.type),
141
+ fbtraceId: str(err?.fbtrace_id),
142
+ path,
143
+ usage,
144
+ });
145
+ }
146
+ if (body === null || typeof body !== "object") {
147
+ throw new InstagramApiError({
148
+ message: `Instagram API returned a response that is not JSON at ${path}`,
149
+ status: res.status,
150
+ path,
151
+ usage,
152
+ });
153
+ }
154
+ return body;
155
+ }
156
+ function endpoint(path, params = {}) {
157
+ const url = new URL(`${baseUrl}${path}`);
158
+ for (const [k, v] of Object.entries(params))
159
+ if (v !== undefined)
160
+ url.searchParams.set(k, v);
161
+ return url;
162
+ }
163
+ /** Accept a `paging.next` URL only when it points at the same Graph host. */
164
+ function nextUrl(next) {
165
+ if (!next)
166
+ return null;
167
+ let url;
168
+ try {
169
+ url = new URL(next);
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ return url.origin === GRAPH_HOST ? url : null;
175
+ }
176
+ async function mediaInsights(mediaId, metrics, signal) {
177
+ const id = checkId(mediaId, "mediaId");
178
+ const body = await call(endpoint(`/${id}/insights`, { metric: metrics.join(",") }), signal);
179
+ const values = metricValues(body);
180
+ return (name) => values.get(name) ?? null;
181
+ }
182
+ function mediaParams(o) {
183
+ return {
184
+ fields: MEDIA_FIELDS.join(","),
185
+ limit: o.limit !== undefined ? String(o.limit) : undefined,
186
+ since: o.since !== undefined ? String(toUnixSeconds(o.since)) : undefined,
187
+ until: o.until !== undefined ? String(toUnixSeconds(o.until)) : undefined,
188
+ after: o.after,
189
+ };
190
+ }
191
+ function toPage(body) {
192
+ return {
193
+ data: (body.data ?? []).map(toMedia),
194
+ after: body.paging?.cursors?.after ?? null,
195
+ hasNext: Boolean(body.paging?.next),
196
+ };
197
+ }
198
+ const client = {
199
+ apiVersion,
200
+ baseUrl,
201
+ get lastUsage() {
202
+ return lastUsage;
203
+ },
204
+ async account(o) {
205
+ const raw = await call(endpoint(`/${userId(o)}`, { fields: ACCOUNT_FIELDS.join(",") }), o?.signal);
206
+ return {
207
+ id: String(raw.id ?? userId(o)),
208
+ username: str(raw.username),
209
+ name: str(raw.name),
210
+ followers_count: num(raw.followers_count),
211
+ follows_count: num(raw.follows_count),
212
+ media_count: num(raw.media_count),
213
+ profile_picture_url: str(raw.profile_picture_url),
214
+ };
215
+ },
216
+ async stories(o) {
217
+ const out = [];
218
+ let url = endpoint(`/${userId(o)}/stories`, { fields: MEDIA_FIELDS.join(",") });
219
+ while (url) {
220
+ const body = await call(url, o?.signal);
221
+ out.push(...(body.data ?? []).map(toMedia));
222
+ url = nextUrl(body.paging?.next);
223
+ }
224
+ return out;
225
+ },
226
+ async media(o = {}) {
227
+ return toPage(await call(endpoint(`/${userId(o)}/media`, mediaParams(o)), o.signal));
228
+ },
229
+ async *iterateMedia(o = {}) {
230
+ const max = o.max ?? Infinity;
231
+ let count = 0;
232
+ let url = endpoint(`/${userId(o)}/media`, mediaParams(o));
233
+ while (url && count < max) {
234
+ const body = await call(url, o.signal);
235
+ for (const raw of body.data ?? []) {
236
+ yield toMedia(raw);
237
+ if (++count >= max)
238
+ return;
239
+ }
240
+ url = nextUrl(body.paging?.next);
241
+ }
242
+ },
243
+ async storyInsights(mediaId, o) {
244
+ const get = await mediaInsights(mediaId, STORY_METRICS, o?.signal);
245
+ return { views: get("views"), reach: get("reach"), replies: get("replies"), shares: get("shares") };
246
+ },
247
+ async postInsights(mediaId, o) {
248
+ const get = await mediaInsights(mediaId, POST_METRICS, o?.signal);
249
+ return {
250
+ views: get("views"),
251
+ reach: get("reach"),
252
+ likes: get("likes"),
253
+ comments: get("comments"),
254
+ saves: get("saved"),
255
+ shares: get("shares"),
256
+ };
257
+ },
258
+ async reelInsights(mediaId, o) {
259
+ const get = await mediaInsights(mediaId, REEL_METRICS, o?.signal);
260
+ return {
261
+ views: get("views"),
262
+ reach: get("reach"),
263
+ likes: get("likes"),
264
+ comments: get("comments"),
265
+ saves: get("saved"),
266
+ shares: get("shares"),
267
+ };
268
+ },
269
+ async accountInsights(o = {}) {
270
+ const metrics = o.metrics ?? DEFAULT_ACCOUNT_METRICS;
271
+ if (metrics.length === 0)
272
+ throw new TypeError("metrics must not be empty");
273
+ const metricType = o.metricType === undefined ? "total_value" : o.metricType;
274
+ const body = await call(endpoint(`/${userId(o)}/insights`, {
275
+ metric: metrics.join(","),
276
+ period: o.period ?? "day",
277
+ metric_type: metricType ?? undefined,
278
+ since: o.since !== undefined ? String(toUnixSeconds(o.since)) : undefined,
279
+ until: o.until !== undefined ? String(toUnixSeconds(o.until)) : undefined,
280
+ breakdown: o.breakdown,
281
+ timeframe: o.timeframe,
282
+ }), o.signal);
283
+ return (body.data ?? []).map((row) => ({
284
+ name: row.name ?? "",
285
+ period: str(row.period),
286
+ title: str(row.title),
287
+ description: str(row.description),
288
+ total: num(row.total_value?.value),
289
+ values: (row.values ?? []).map((v) => ({ value: v.value ?? null, endTime: str(v.end_time) })),
290
+ breakdowns: (row.total_value?.breakdowns ?? []).map((b) => ({
291
+ dimensionKeys: b.dimension_keys ?? [],
292
+ results: (b.results ?? [])
293
+ .filter((r) => Array.isArray(r.dimension_values) && num(r.value) !== null)
294
+ .map((r) => ({ dimensionValues: r.dimension_values, value: r.value })),
295
+ })),
296
+ }));
297
+ },
298
+ };
299
+ return client;
300
+ }
@@ -0,0 +1,50 @@
1
+ import type { RateLimitUsage } from "./usage.js";
2
+ /**
3
+ * Meta error codes for rate limits: 4 (app), 17 (user), 32 (page),
4
+ * 613 (call rate), 80001 (Pages use case), 80002 (Instagram use case).
5
+ */
6
+ export declare const RATE_LIMIT_CODES: ReadonlySet<number>;
7
+ /** Code 190: the access token is expired, revoked or not valid. */
8
+ export declare const AUTH_ERROR_CODES: ReadonlySet<number>;
9
+ /**
10
+ * Subcodes that mean the user must log in again: 458 (app removed),
11
+ * 460 (password changed), 463 (token expired), 467 (token not valid).
12
+ */
13
+ export declare const AUTH_ERROR_SUBCODES: ReadonlySet<number>;
14
+ export type InstagramApiErrorInit = {
15
+ message: string;
16
+ status: number;
17
+ code?: number | null;
18
+ subcode?: number | null;
19
+ type?: string | null;
20
+ fbtraceId?: string | null;
21
+ path?: string | null;
22
+ usage?: RateLimitUsage | null;
23
+ };
24
+ /**
25
+ * A failed Graph API call. `message` never contains the access token.
26
+ * `status` is 0 when the request did not reach Meta (network error or timeout).
27
+ */
28
+ export declare class InstagramApiError extends Error {
29
+ /** The HTTP status. 0 for a network error or a timeout. */
30
+ readonly status: number;
31
+ /** Meta's `error.code`. */
32
+ readonly code: number | null;
33
+ /** Meta's `error.error_subcode`. */
34
+ readonly subcode: number | null;
35
+ /** Meta's `error.type`, for example `OAuthException`. */
36
+ readonly type: string | null;
37
+ /** Meta's `error.fbtrace_id`. Give it to Meta support when you report a problem. */
38
+ readonly fbtraceId: string | null;
39
+ /** The API path that failed, without the query string. */
40
+ readonly path: string | null;
41
+ /** The rate-limit usage headers of the failed response, if Meta sent them. */
42
+ readonly usage: RateLimitUsage | null;
43
+ constructor(init: InstagramApiErrorInit);
44
+ /** True when Meta throttled the call. Wait before you try again. */
45
+ get isRateLimit(): boolean;
46
+ /** True when the token is expired or not valid (code 190 or an auth subcode). Get a new token. */
47
+ get isAuthError(): boolean;
48
+ }
49
+ /** Replace every copy of `secret` in `text` with `[redacted]`. */
50
+ export declare function redact(text: string, secret: string | undefined): string;
package/dist/errors.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Meta error codes for rate limits: 4 (app), 17 (user), 32 (page),
3
+ * 613 (call rate), 80001 (Pages use case), 80002 (Instagram use case).
4
+ */
5
+ export const RATE_LIMIT_CODES = new Set([4, 17, 32, 613, 80001, 80002]);
6
+ /** Code 190: the access token is expired, revoked or not valid. */
7
+ export const AUTH_ERROR_CODES = new Set([190]);
8
+ /**
9
+ * Subcodes that mean the user must log in again: 458 (app removed),
10
+ * 460 (password changed), 463 (token expired), 467 (token not valid).
11
+ */
12
+ export const AUTH_ERROR_SUBCODES = new Set([458, 460, 463, 467]);
13
+ /**
14
+ * A failed Graph API call. `message` never contains the access token.
15
+ * `status` is 0 when the request did not reach Meta (network error or timeout).
16
+ */
17
+ export class InstagramApiError extends Error {
18
+ /** The HTTP status. 0 for a network error or a timeout. */
19
+ status;
20
+ /** Meta's `error.code`. */
21
+ code;
22
+ /** Meta's `error.error_subcode`. */
23
+ subcode;
24
+ /** Meta's `error.type`, for example `OAuthException`. */
25
+ type;
26
+ /** Meta's `error.fbtrace_id`. Give it to Meta support when you report a problem. */
27
+ fbtraceId;
28
+ /** The API path that failed, without the query string. */
29
+ path;
30
+ /** The rate-limit usage headers of the failed response, if Meta sent them. */
31
+ usage;
32
+ constructor(init) {
33
+ super(init.message);
34
+ this.name = "InstagramApiError";
35
+ this.status = init.status;
36
+ this.code = init.code ?? null;
37
+ this.subcode = init.subcode ?? null;
38
+ this.type = init.type ?? null;
39
+ this.fbtraceId = init.fbtraceId ?? null;
40
+ this.path = init.path ?? null;
41
+ this.usage = init.usage ?? null;
42
+ }
43
+ /** True when Meta throttled the call. Wait before you try again. */
44
+ get isRateLimit() {
45
+ return this.code !== null && RATE_LIMIT_CODES.has(this.code);
46
+ }
47
+ /** True when the token is expired or not valid (code 190 or an auth subcode). Get a new token. */
48
+ get isAuthError() {
49
+ if (this.code !== null && AUTH_ERROR_CODES.has(this.code))
50
+ return true;
51
+ return this.subcode !== null && AUTH_ERROR_SUBCODES.has(this.subcode);
52
+ }
53
+ }
54
+ /** Replace every copy of `secret` in `text` with `[redacted]`. */
55
+ export function redact(text, secret) {
56
+ if (!secret || secret.length < 4)
57
+ return text;
58
+ let out = text.split(secret).join("[redacted]");
59
+ const encoded = encodeURIComponent(secret);
60
+ if (encoded !== secret)
61
+ out = out.split(encoded).join("[redacted]");
62
+ return out;
63
+ }
@@ -0,0 +1,7 @@
1
+ export { createClient, DEFAULT_API_VERSION, GRAPH_HOST, MEDIA_FIELDS, ACCOUNT_FIELDS, STORY_METRICS, POST_METRICS, REEL_METRICS, DEFAULT_ACCOUNT_METRICS, } from "./client.js";
2
+ export type { ClientOptions, CallOptions, InstagramClient, Account, Media, MediaType, MediaProductType, MediaOptions, MediaPage, StoryInsights, PostInsights, ReelInsights, AccountInsightsOptions, AccountInsight, AccountInsightBreakdown, } from "./client.js";
3
+ export { InstagramApiError, RATE_LIMIT_CODES, AUTH_ERROR_CODES, AUTH_ERROR_SUBCODES, } from "./errors.js";
4
+ export type { InstagramApiErrorInit } from "./errors.js";
5
+ export { parseUsageHeaders } from "./usage.js";
6
+ export type { RateLimitUsage, AppUsage, BusinessUseCaseUsage } from "./usage.js";
7
+ export type { TimeInput } from "./time.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createClient, DEFAULT_API_VERSION, GRAPH_HOST, MEDIA_FIELDS, ACCOUNT_FIELDS, STORY_METRICS, POST_METRICS, REEL_METRICS, DEFAULT_ACCOUNT_METRICS, } from "./client.js";
2
+ export { InstagramApiError, RATE_LIMIT_CODES, AUTH_ERROR_CODES, AUTH_ERROR_SUBCODES, } from "./errors.js";
3
+ export { parseUsageHeaders } from "./usage.js";
package/dist/time.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A point in time: a `Date`, epoch milliseconds, or a date string such as
3
+ * `2026-10-01` or `2026-10-01T12:00:00Z`.
4
+ */
5
+ export type TimeInput = Date | number | string;
6
+ /** Convert a `TimeInput` to Unix seconds, the unit Graph API `since`/`until` use. */
7
+ export declare function toUnixSeconds(input: TimeInput): number;
package/dist/time.js ADDED
@@ -0,0 +1,8 @@
1
+ /** Convert a `TimeInput` to Unix seconds, the unit Graph API `since`/`until` use. */
2
+ export function toUnixSeconds(input) {
3
+ const ms = input instanceof Date ? input.getTime() : typeof input === "number" ? input : Date.parse(input);
4
+ if (!Number.isFinite(ms)) {
5
+ throw new TypeError(`Not a valid date: ${String(input)}`);
6
+ }
7
+ return Math.floor(ms / 1000);
8
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Meta's rate-limit usage headers. Each value is a percentage of the limit
3
+ * (0-100). Meta throttles when any one of them reaches 100.
4
+ */
5
+ export type AppUsage = {
6
+ call_count: number;
7
+ total_time: number;
8
+ total_cputime: number;
9
+ };
10
+ export type BusinessUseCaseUsage = AppUsage & {
11
+ /** The use case, for example `instagram` or `pages`. */
12
+ type: string;
13
+ /** Minutes until the app can call again. 0 when not throttled. */
14
+ estimated_time_to_regain_access: number;
15
+ };
16
+ export type RateLimitUsage = {
17
+ /** From `x-app-usage`. Null when the header is missing. */
18
+ app: AppUsage | null;
19
+ /** From `x-business-use-case-usage`, keyed by business object id. */
20
+ businessUseCase: Record<string, BusinessUseCaseUsage[]>;
21
+ };
22
+ /** Parse `x-app-usage` and `x-business-use-case-usage`. Bad JSON gives empty values. */
23
+ export declare function parseUsageHeaders(headers: Headers): RateLimitUsage;
package/dist/usage.js ADDED
@@ -0,0 +1,55 @@
1
+ function parseJson(raw) {
2
+ if (!raw)
3
+ return null;
4
+ try {
5
+ return JSON.parse(raw);
6
+ }
7
+ catch {
8
+ return null;
9
+ }
10
+ }
11
+ function pct(n) {
12
+ return typeof n === "number" && Number.isFinite(n) ? Math.max(0, n) : 0;
13
+ }
14
+ function toAppUsage(raw) {
15
+ if (!raw || typeof raw !== "object")
16
+ return null;
17
+ const o = raw;
18
+ return {
19
+ call_count: pct(o.call_count),
20
+ total_time: pct(o.total_time),
21
+ total_cputime: pct(o.total_cputime),
22
+ };
23
+ }
24
+ function toBuc(raw) {
25
+ if (!raw || typeof raw !== "object")
26
+ return {};
27
+ const out = {};
28
+ for (const [id, entries] of Object.entries(raw)) {
29
+ if (!Array.isArray(entries))
30
+ continue;
31
+ const parsed = [];
32
+ for (const e of entries) {
33
+ if (!e || typeof e !== "object")
34
+ continue;
35
+ const o = e;
36
+ parsed.push({
37
+ type: typeof o.type === "string" ? o.type : "unknown",
38
+ call_count: pct(o.call_count),
39
+ total_time: pct(o.total_time),
40
+ total_cputime: pct(o.total_cputime),
41
+ estimated_time_to_regain_access: pct(o.estimated_time_to_regain_access),
42
+ });
43
+ }
44
+ if (parsed.length > 0)
45
+ out[id] = parsed;
46
+ }
47
+ return out;
48
+ }
49
+ /** Parse `x-app-usage` and `x-business-use-case-usage`. Bad JSON gives empty values. */
50
+ export function parseUsageHeaders(headers) {
51
+ return {
52
+ app: toAppUsage(parseJson(headers.get("x-app-usage"))),
53
+ businessUseCase: toBuc(parseJson(headers.get("x-business-use-case-usage"))),
54
+ };
55
+ }
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@storylinkpro/instagram-stories-api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Instagram Graph API client and CLI for Node.js and TypeScript: read Instagram Stories, posts, reels and their insights (views, reach, replies, likes, saves, shares). Typed, zero dependencies.",
5
+ "keywords": [
6
+ "instagram",
7
+ "instagram-api",
8
+ "instagram-graph-api",
9
+ "instagram-stories",
10
+ "instagram-story",
11
+ "instagram-insights",
12
+ "instagram-analytics",
13
+ "instagram-reels",
14
+ "instagram-business",
15
+ "meta-graph-api",
16
+ "facebook-graph-api",
17
+ "graph-api",
18
+ "social-media",
19
+ "analytics",
20
+ "insights",
21
+ "api-client",
22
+ "cli",
23
+ "typescript",
24
+ "nodejs",
25
+ "zero-dependency"
26
+ ],
27
+ "homepage": "https://github.com/storylinkpro/instagram-stories-api#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/storylinkpro/instagram-stories-api/issues"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/storylinkpro/instagram-stories-api.git"
34
+ },
35
+ "license": "MIT",
36
+ "author": "Storylink (https://storylink.pro)",
37
+ "type": "module",
38
+ "main": "dist/index.js",
39
+ "types": "dist/index.d.ts",
40
+ "bin": {
41
+ "instagram-stories-api": "dist/bin.js"
42
+ },
43
+ "exports": {
44
+ ".": {
45
+ "types": "./dist/index.d.ts",
46
+ "import": "./dist/index.js"
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "files": [
51
+ "CHANGELOG.md",
52
+ "LICENSE",
53
+ "README.md",
54
+ "dist"
55
+ ],
56
+ "sideEffects": false,
57
+ "engines": {
58
+ "node": ">=20"
59
+ },
60
+ "scripts": {
61
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
62
+ "typecheck": "tsc --noEmit",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "check": "npm run typecheck && npm test && npm run build",
66
+ "prepublishOnly": "npm run check"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "22.19.1",
70
+ "typescript": "5.9.3",
71
+ "vitest": "5.0.3"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ }
76
+ }