@apicity/ meta (中文(简体) )
由Instagram Graph API提供商通过公-URL流(graph.instagram.com)发布回路.
运行时间依赖性 :
zod@^4.6.6- 请求在每一个 POST 端点上附加计划.schema
安装
npm install @apicity/meta
# or
pnpm add @apicity/meta
快速启动
import { createMeta } from "@apicity/meta";
const meta = createMeta({ accessToken: process.env.IG_ACCESS_TOKEN! });
设置
Instagram 需要一个 长寿(60天)用户访问符 从 Instagram 登录 OAuth 流. 符号绑定在一个特定的Instagram上 企业或创建者账户;个人账户没有方案 出版访问.
1. 配置您的 Meta App
打开 开发者. facebook.com创建一个 新应用( 类型 : 商业,然后添加 内存 产品. 选择 使用 Instagram 登录的 API 设置 并配置 :
- 有效的 OAuth 重定向 URI :
http://127.0.0.1:8765/callback - 权限 :
instagram_business_basic+ 键instagram_business_content_publish - 保存该 Instagram 应用程序标识 财务报告和审定财务报表 Instagram 应用程序机密
2. 验证Instagram账户类型
你所出版的Instagram帐户一定是 商业 或者说 创建者 (不是个人的). 在Instagram 移动应用程序下切换 设置 ~ 账户类型和工具. 不需要Facebook页面链接 用于Instagram登录流.
3. 薄荷长期使用
将脚本另存为 mint-ig-token.mjs 运行它:
IG_CLIENT_ID=<your-instagram-app-id> \
IG_CLIENT_SECRET=<your-instagram-app-secret> \
node mint-ig-token.mjs
它打印一个授权 URL —— 打开, 点击 允许和帮助者
抓取方向 127.0.0.1:8765代码交换
然后升级为长寿(60天)的标志
指纹 { access_token, user_id, expires_in }保存两个 access_token
财务报告和审定财务报表 user_id - 你们两个都要给API打电话.
mint-ig-token.mjs — 0 Dep OAuth 2.0 帮助器
import http from "node:http";
import crypto from "node:crypto";
const CLIENT_ID = process.env.IG_CLIENT_ID;
const CLIENT_SECRET = process.env.IG_CLIENT_SECRET;
const REDIRECT = "http://127.0.0.1:8765/callback";
const SCOPES = [
"instagram_business_basic",
"instagram_business_content_publish",
].join(",");
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error("Set IG_CLIENT_ID and IG_CLIENT_SECRET");
process.exit(1);
}
const state = crypto.randomBytes(16).toString("hex");
const authURL = new URL("https://www.instagram.com/oauth/authorize");
authURL.searchParams.set("client_id", CLIENT_ID);
authURL.searchParams.set("redirect_uri", REDIRECT);
authURL.searchParams.set("response_type", "code");
authURL.searchParams.set("scope", SCOPES);
authURL.searchParams.set("state", state);
console.log("Open this URL and click Allow:\n" + authURL.toString());
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, REDIRECT);
if (!url.pathname.startsWith("/callback")) {
res.writeHead(404).end();
return;
}
const code = url.searchParams.get("code");
if (!code || url.searchParams.get("state") !== state) {
res.writeHead(400).end("bad state");
server.close();
process.exit(1);
}
// 1. short-lived
const shortRes = await fetch(
"https://api.instagram.com/oauth/access_token",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: "authorization_code",
redirect_uri: REDIRECT,
code,
}),
}
);
const short = await shortRes.json();
// 2. long-lived (60-day)
const longURL = new URL("https://graph.instagram.com/access_token");
longURL.searchParams.set("grant_type", "ig_exchange_token");
longURL.searchParams.set("client_secret", CLIENT_SECRET);
longURL.searchParams.set("access_token", short.access_token);
const longRes = await fetch(longURL);
const long = await longRes.json();
console.log(JSON.stringify({
access_token: long.access_token,
expires_in: long.expires_in,
user_id: short.user_id,
}, null, 2));
res.writeHead(200).end("Authorized — check your terminal.");
server.close();
});
server.listen(8765, "127.0.0.1");
4. 使用标志
import { createMeta } from "@apicity/meta";
const meta = createMeta({ accessToken: process.env.IG_ACCESS_TOKEN! });
const igUserId = process.env.IG_USER_ID!;
// Public-URL flow: host the mp4 somewhere (e.g. via @apicity/free-media-upload) and
// pass its URL. Meta GETs the video and processes it asynchronously.
const container = await meta.post.v25.media(igUserId, {
media_type: "REELS",
video_url: "https://example.com/clip.mp4",
caption: "hello from @apicity/meta",
});
// Poll until the container is ready.
let status = "IN_PROGRESS";
while (status === "IN_PROGRESS") {
await new Promise((r) => setTimeout(r, 5000));
const s = await meta.get.v25.container(container.id, {
fields: "status_code,status",
});
status = s.status_code ?? "FINISHED";
}
// Publish.
const post = await meta.post.v25.mediaPublish(igUserId, {
creation_id: container.id,
});
console.log(post.id);
现实世界的例子:通过公共-URL流发布一个 Reel
Instagram的Graph API不直接取视频字节——Meta需要一个
它可以公开达到的 URL GET 并同步转码。 这个
链条下方片段 @apicity/free-media-upload (盒式公共托管,免费+0)
auth)为 @apicity/meta 着陆a mp4 在磁盘作为出版的里尔,
镜像
tests/integration/ig-post-video.test.ts
步为正. 收件箱上传反弹
tests/recordings/free_2578706139/· ;
iG呼叫登陆 tests/recordings/ig_*/post-video_*/recording.har
商业/创建账户 IG_ACCESS_TOKEN 被记录.
import { readFileSync } from "node:fs";
import { createMeta } from "@apicity/meta";
import { createFreeMediaUpload } from "@apicity/free-media-upload";
const meta = createMeta({ accessToken: process.env.IG_ACCESS_TOKEN! });
const igUserId = process.env.IG_USER_ID!; // 17-digit numeric, e.g. "17841471234567890"
// 1. Host the mp4 publicly. catbox.moe is auth-free and persistent —
// Meta's transcode worker will fetch this URL once during step 2,
// so any host that returns the bytes within ~30s works (S3 presigned
// URL, R2, your own CDN). @apicity/free-media-upload wraps the multipart upload
// and returns the resolved file URL as a string.
const bytes = readFileSync("./jump.mp4");
const blob = new Blob([bytes], { type: "video/mp4" });
const freeMediaUpload = createFreeMediaUpload({});
const videoUrl = await freeMediaUpload.catbox.upload({
file: blob,
filename: "jump.mp4",
});
console.log(videoUrl);
// → "https://files.catbox.moe/nn9sei.mp4"
// catbox returns a permanent public URL of the form
// `https://files.catbox.moe/<6char>.<ext>` — that's what Meta will GET.
// 2. Create a media container. For Reels you must pass `media_type:
// "REELS"` (NOT "VIDEO" — that's the legacy IGTV path Meta
// deprecated in 2024). The container is a server-side handle: Meta
// queues the transcode against `video_url` and returns its id
// immediately. Containers expire 24h after creation if you don't
// publish them.
const container = await meta.post.v25.media(igUserId, {
media_type: "REELS",
video_url: videoUrl,
caption: "jump #reels",
});
console.log(container.id);
// → "17889012345678901" (17-digit container id)
// 3. Poll the container's status_code until it leaves IN_PROGRESS.
// The state machine is IN_PROGRESS → FINISHED on success;
// FINISHED is the only state media_publish accepts. ERROR and
// EXPIRED are terminal failure states; PUBLISHED is what you'll
// see if you re-poll AFTER calling media_publish. The `fields`
// query param is required — by default the GET only returns `id`.
let statusCode: string = "IN_PROGRESS";
while (statusCode === "IN_PROGRESS") {
await new Promise((r) => setTimeout(r, 5000));
const s = await meta.get.v25.container(container.id, {
fields: "status_code,status",
});
statusCode = s.status_code ?? "FINISHED";
if (statusCode === "ERROR" || statusCode === "EXPIRED") {
throw new Error(`container ${container.id} ${statusCode}: ${s.status}`);
}
}
// statusCode → "FINISHED"
// s.status → "Finished: Media is ready to be published."
// 4. Publish. media_publish takes the container id (NOT the media url)
// and returns the new post's id — that's the permanent ig_id you'd
// use to construct an https://www.instagram.com/reel/<shortcode>/
// URL or to query insights later via Graph API.
const post = await meta.post.v25.mediaPublish(igUserId, {
creation_id: container.id,
});
console.log(post.id);
// → "17912345678901234" (17-digit post id, distinct from container id)
页:1
- Meta 需要一个 商业 或者说 创建者 Instagram 账户加 a
已核准
instagram_business_content_publish个人 账户获取190("无效 OAuth 访问符") 实用有效的符号 . video_url必须能够从 Meta 的 IP 中达到并服务Content-Type: video/mp4。常见的获取chas:在 转码器拉出,主机需要User-Agent,而CDNs则 重定向到另一个来源 。 {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}三边都躲过了.- Reel本身必须满足Meta的Reel限制——9:16方面,
3–90s 持续时间 QQ 1GB,H.264视频,AAC音频. 表面错配为
status_code: "ERROR"在民意调查中, 与人可读 原因在其中status(例如:."Error: The video is too short.") (中文(简体) ). - 集装箱和货站的使用 独立 17个数字的ID. 集装箱编号
- 你只需要它去投票和随后的投票
media_publishcreation_id。后 ID 是永久的并存活下来 用户删除该帖. - 丢出错误
MetaError与status(HTTP代码),body(解说) 元错误信封),和一个可选code。Meta 的两个错误形状 —— 说error.error_user_msg用于用户的验证和error.message对于其他的一切, 都浮出水面MetaError.message,所以一个 单人try/catch自然读取.
API 参考
3个端点跨出3个组. 每个方法都映射出上游的 URL 路径 .
集装箱
GET meta.v25.container
GET https://graph.instagram.com/v25.0/{containerId}{query}
成本等级 : cheap
const res = await meta.get.v25.container({ /* ... */ });
媒体
POST meta.v25.media
POST https://graph.instagram.com/v25.0/{igUserId}/media
成本等级 : cheap
const res = await meta.post.v25.media({ /* ... */ });
媒体
POST meta.v25.mediaPublish
POST https://graph.instagram.com/v25.0/{igUserId}/media_publish
成本等级 : cheap
const res = await meta.post.v25.mediaPublish({ /* ... */ });
部分 辣点 单相机.
许可证
麻省理工学院——见 自由主义。 。 。 .