Instagram WebSocket
使用 TweetStream API key 连接 wss://insta-ws.tweetstream.io/ws,接收监控列表中 Instagram 账号新发布的帖子、Reels、轮播帖和快拍。
快速连接
你需要有效的 Instagram Tracking 订阅或试用、控制台监控列表中至少一个 Instagram 账号,以及 TweetStream API key。运行下面的示例:输出 Instagram feed connected 表示认证成功,监控账号发布内容时会打印事件。
| 设置 | 值 | 说明 |
|---|---|---|
| 端点 | wss://insta-ws.tweetstream.io/ws | 与 X 数据流端点分开 |
| 子协议 | tweetstream.instagram.v1 | 只要发送任何子协议(包括认证令牌)就必须提供。其他子协议返回 HTTP 400 |
| 认证协议 | tweetstream.auth.token.<API_KEY> | 使用现有的 TweetStream API key |
运行 TypeScript 冒烟测试
typescript// bun add ws
// TWEETSTREAM_API_KEY=ts_... bun run instagram.ts
import WebSocket from "ws";
const apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) {
throw new Error("Missing TWEETSTREAM_API_KEY");
}
const ws = new WebSocket("wss://insta-ws.tweetstream.io/ws", [
"tweetstream.instagram.v1",
`tweetstream.auth.token.${apiKey}`,
]);
ws.on("open", () => {
console.log("Instagram feed connected");
});
ws.on("message", (raw) => {
const event = JSON.parse(raw.toString());
if (event.t === "instagram" && (event.op === "content" || event.op === "update")) {
console.log(event.op, event.d.kind, event.d.author.username, event.d.url);
}
});
ws.on("unexpected-response", (_request, response) => {
console.error("Connection rejected", response.statusCode, response.statusMessage);
response.resume();
process.exitCode = 1;
});
ws.on("close", (code, reason) => {
console.log("Instagram feed closed", code, reason.toString());
});认证
Instagram WebSocket 接受与 X 数据流相同的 API key 和认证方式。如果以 tweetstream.auth.token.<API_KEY> 发送 key,还需同时提供 tweetstream.instagram.v1。请把 key 保存在服务端,不要放进公开的浏览器代码。
| 场景 | 请求头或协议 | 说明 |
|---|---|---|
| WebSocket | tweetstream.instagram.v1 + tweetstream.auth.token.<API_KEY> | 推荐方式 |
| WebSocket 备用方式 | Authorization: Bearer <API_KEY> 或 ?apiKey=<API_KEY> | 只有运行环境无法设置请求头时才使用查询参数 |
读取消息信封
每一帧都是一个 JSON 消息信封,结构与 X 数据流相同。先按 t、再按 op 分流;遇到无法识别的操作时忽略该消息,不要关闭连接。
| 字段 | 类型 | 含义 |
|---|---|---|
v | 1 | 消息信封版本 |
t | instagram 或 control | 频道 |
op | content、update 或 auth_pong | 操作 |
id | string | 媒体 ID。更新事件沿用它所替换的内容事件的 ID |
ts | number | TweetStream 发送该帧的时间,Unix 毫秒 |
d | object | 该操作的载荷 |
Instagram 消息信封类型
显示完整定义(72 行)
type InstagramKind = 'post' | 'reel' | 'carousel' | 'story' | 'unknown';
type InstagramMedia = {
// Position in the media array, starting at 0.
index: number;
type: 'image' | 'video' | 'unknown';
url: string;
// Present only on an image that is a video's cover.
cover?: true;
};
type InstagramContent = {
// Instagram media ID; also the envelope id.
id: string;
// Present when known; the post URL uses it.
shortcode?: string;
kind: InstagramKind;
url: string;
author: {
id?: string;
// Lowercase, without a leading @.
username: string;
};
// Empty string when the post has no caption.
caption: string;
// May be empty or partial; see the media notes.
media: Array<InstagramMedia>;
// Publish time in Unix milliseconds, or null when it is not known.
createdAt: number | null;
// True when createdAt is an estimate.
createdAtEstimated: boolean;
// When TweetStream received the post, in Unix milliseconds.
receivedAt: number;
// Content events only. True when this post reached TweetStream well after its
// publish time; the post is still new to you. A missing flag does not mean
// the post is recent: when createdAt is set, compare it with receivedAt.
late?: true;
};
type InstagramContentEnvelope = {
v: 1;
t: 'instagram';
op: 'content';
id: string;
ts: number;
d: InstagramContent;
};
type InstagramUpdateEnvelope = {
v: 1;
t: 'instagram';
// Insert, or replace the stored event with the same id. Sent at most once per id.
op: 'update';
id: string;
ts: number;
// Never carries late: keep the value from the content event, or compare
// createdAt with receivedAt if you never got one.
d: Omit<InstagramContent, 'late'>;
};
type InstagramAuthPongEnvelope = {
v: 1;
t: 'control';
op: 'auth_pong';
ts: number;
d: Record<string, never>;
};
type InstagramEnvelope =
| InstagramContentEnvelope
| InstagramUpdateEnvelope
| InstagramAuthPongEnvelope;内容类型
已知类型时,d.kind 会说明发布的内容类型。d.url 指向 Instagram 上的内容;未知 shortcode 时指向作者主页。
| 类型 | 含义 | URL 格式 |
|---|---|---|
post | 包含一张图片或一个视频的帖子 | instagram.com/p/<shortcode>/ |
reel | Reels 短视频 | instagram.com/reel/<shortcode>/ |
carousel | 包含多张图片或多个视频的帖子 | instagram.com/p/<shortcode>/ |
story | 快拍 | instagram.com/stories/<username>/<id>/ |
unknown | 未能确定类型。事件仍包含 ID、作者、URL 和已找到的媒体,可按该账号的新内容处理 | instagram.com/p/<shortcode>/;未知 shortcode 时为作者主页 |
媒体顺序与封面
index是该项在media中的位置,从 0 开始。已知帖子的滑动顺序时,media按该顺序排列。图片和视频混合的轮播帖可能先列出图片。cover: true表示该图片是某个视频的封面,其他媒体不含此字段。type为image、video,未能确定时为unknown。media可能为空,或少于帖子实际包含的媒体数量。之后的update可能补全其余媒体。需要完整内容时请打开url。- 媒体 URL 可能过期,请在收到事件后尽快下载所需文件。
时间戳
| 字段 | 类型 | 含义 |
|---|---|---|
createdAt | number 或 null | 发布时间,Unix 毫秒;未知时为 null |
createdAtEstimated | boolean | createdAt 为估算值时为 true |
receivedAt | number | TweetStream 收到该内容的时间,Unix 毫秒 |
late | true 或不出现 | 帖子在发布很久之后才到达 TweetStream 时为 true,它对你来说仍是新帖子。只有 content 事件带此字段,update 不带。没有 late 不代表帖子是最近发布的:createdAt 有值时,请与 receivedAt 比较 |
事件示例
每个示例都是连接中收到的完整一帧。
轮播帖
json{
"v": 1,
"t": "instagram",
"op": "content",
"id": "3988012345678901234",
"ts": 1772000001060,
"d": {
"id": "3988012345678901234",
"shortcode": "DdsPyK1GrTK",
"kind": "carousel",
"url": "https://www.instagram.com/p/DdsPyK1GrTK/",
"author": {
"id": "25025320",
"username": "examplebrand"
},
"caption": "Three frames from launch day",
"media": [
{
"index": 0,
"type": "image",
"url": "https://scontent-iad3-1.cdninstagram.com/v/1.jpg"
},
{
"index": 1,
"type": "video",
"url": "https://scontent-iad3-1.cdninstagram.com/v/2.mp4"
},
{
"index": 2,
"type": "image",
"url": "https://scontent-iad3-1.cdninstagram.com/v/2.jpg",
"cover": true
}
],
"createdAt": 1772000000000,
"createdAtEstimated": false,
"receivedAt": 1772000001050
}
}Reels
json{
"v": 1,
"t": "instagram",
"op": "content",
"id": "3987848477383929083",
"ts": 1772000001060,
"d": {
"id": "3987848477383929083",
"shortcode": "DdXrw_IjSz7",
"kind": "reel",
"url": "https://www.instagram.com/reel/DdXrw_IjSz7/",
"author": {
"id": "25025320",
"username": "examplebrand"
},
"caption": "New drop",
"media": [
{
"index": 0,
"type": "video",
"url": "https://scontent-iad3-1.cdninstagram.com/v/reel.mp4"
}
],
"createdAt": 1772000000000,
"createdAtEstimated": false,
"receivedAt": 1772000001050
}
}发布时间为估算值的快拍
json{
"v": 1,
"t": "instagram",
"op": "content",
"id": "3988000000000000001",
"ts": 1772000001060,
"d": {
"id": "3988000000000000001",
"kind": "story",
"url": "https://www.instagram.com/stories/examplebrand/3988000000000000001/",
"author": {
"id": "25025320",
"username": "examplebrand"
},
"caption": "",
"media": [
{
"index": 0,
"type": "image",
"url": "https://scontent-iad3-1.cdninstagram.com/v/story.jpg"
}
],
"createdAt": 1772000000000,
"createdAtEstimated": true,
"receivedAt": 1772000001050
}
}未确定类型
json{
"v": 1,
"t": "instagram",
"op": "content",
"id": "3987848477383929083",
"ts": 1772000001060,
"d": {
"id": "3987848477383929083",
"shortcode": "DdXrw_IjSz7",
"kind": "unknown",
"url": "https://www.instagram.com/p/DdXrw_IjSz7/",
"author": {
"id": "25025320",
"username": "examplebrand"
},
"caption": "Kind not established",
"media": [
{
"index": 0,
"type": "unknown",
"url": "https://scontent-iad3-1.cdninstagram.com/v/first.jpg"
}
],
"createdAt": null,
"createdAtEstimated": false,
"receivedAt": 1772000001050
}
}迟到的帖子
json{
"v": 1,
"t": "instagram",
"op": "content",
"id": "3987848477383929083",
"ts": 1772000001060,
"d": {
"id": "3987848477383929083",
"shortcode": "DdXrw_IjSz7",
"kind": "post",
"url": "https://www.instagram.com/p/DdXrw_IjSz7/",
"author": {
"id": "25025320",
"username": "examplebrand"
},
"caption": "Behind the scenes",
"createdAt": 1771999101050,
"media": [
{
"type": "image",
"url": "https://scontent-iad3-1.cdninstagram.com/v/late.jpg",
"index": 0
}
],
"createdAtEstimated": false,
"late": true,
"receivedAt": 1772000001050
}
}更新
update 事件携带该 id 的完整 d 载荷。如果你已保存该 id 的事件,请用新事件整体替换,不要逐字段合并。如果尚未见过该 id(例如在首个事件之后才连接),请将该更新当作新内容处理。每个事件最多发送一次更新,且仅在 TweetStream 获得更完整的版本时发送,例如补充了媒体或确定了类型。更新只通过 WebSocket 发送,Discord 中的帖子不会被修改。
轮播帖更新
json{
"v": 1,
"t": "instagram",
"op": "update",
"id": "3988012345678901234",
"ts": 1772000001060,
"d": {
"id": "3988012345678901234",
"shortcode": "DdsPyK1GrTK",
"kind": "carousel",
"url": "https://www.instagram.com/p/DdsPyK1GrTK/",
"author": {
"id": "25025320",
"username": "examplebrand"
},
"caption": "Three frames from launch day",
"createdAt": 1772000000000,
"media": [
{
"type": "image",
"url": "https://scontent-iad3-1.cdninstagram.com/v/1.jpg",
"index": 0
},
{
"type": "video",
"url": "https://scontent-iad3-1.cdninstagram.com/v/2.mp4",
"index": 1
},
{
"type": "image",
"url": "https://scontent-iad3-1.cdninstagram.com/v/2.jpg",
"index": 2,
"cover": true
}
],
"createdAtEstimated": false,
"receivedAt": 1772000001050
}
}检查认证
应用需要检查认证时,发送 { "op": "auth_ping" }。连接有效时会返回 control/auth_pong,data 为空对象。
认证 ping
json{
"op": "auth_ping"
}认证 pong
json{
"v": 1,
"t": "control",
"op": "auth_pong",
"ts": 1772000001060,
"d": {}
}通过 HTTP 管理账号
用 API key 修改的就是控制台中显示的同一个监控列表。accounts 可以是一个用户名,也可以是最多 100 个用户名的数组,带不带 @ 均可。207 表示部分账号已修改、部分被拒绝,rejected 会给出每个账号的原因。添加已在列表中的账号会返回 duplicate,移除不在列表中的账号会返回 not_tracked。两者都不算失败,因此可以放心重试请求。
| 方法 | 路径 | 结果 |
|---|---|---|
GET | /api/instagram/accounts | 你的账号(状态为 live、pending 或 paused)以及 limit |
POST | /api/instagram/add-account | 添加账号。超出上限的账号以 capacity 返回 |
DELETE | /api/instagram/remove-account | 移除账号,并在 removed 中列出 |
添加 Instagram 账号
typescriptconst apiKey = process.env.TWEETSTREAM_API_KEY;
if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY");
const response = await fetch("https://api.tweetstream.io/api/instagram/add-account", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
accounts: ["nasa", "@natgeo"],
}),
});
if (!response.ok && response.status !== 207) {
const detail = await response.text();
throw new Error(`Add failed (${response.status}): ${detail}`);
}
const { accounts, limit, rejected } = await response.json();
console.log(`${accounts.length}/${limit} accounts`, rejected);连接与账号
- 每个 Instagram Tracking 订阅包含 3 个 Instagram WebSocket 连接,试用期间为 1 个。它们与 X 套餐的连接数分开计算。
- 可随时在控制台或通过 HTTP 添加或移除 Instagram 账号,账号变动不会改变账单。
- 事件也可以发送到 Discord 频道,请在控制台中设置。