social-website-api
v0.2.2
Published
Browser-backed website-api integrations for Facebook, Instagram, Rednote, TikTok, X, and YouTube.
Maintainers
Readme
social-website-api
Reusable social media integrations for
website-api(Facebook, etc.) using your signed-in Chrome session.
Overview
social-website-api provides pre-built site definitions for extracting data from social media platforms using website-api.
One provider is one site, and each thing it does is an operation. A provider is one signed-in identity, so the session — domain, transport, cookies — belongs to the site, while every input belongs to the single operation that reads it. You name the operation you want:
npx website-api facebook --help # the 9 operations Facebook offers
npx website-api facebook post --profile zuck # by name…
npx website-api facebook --operation post --profile zuck # …or by flag
npx website-api facebook create-post --help # only this operation's flagsA suite with several operations requires you to name one, so a create is never reached by accident, and a flag belonging to another operation is reported as such rather than as "unknown option".
| Site | Operations |
| --- | --- |
| facebook | post, post-detail, create-post, create-job, create-live-video, create-story, create-reel, create-life-update, create-note |
| instagram | profile, post, create-post |
| x | account, post, create-post |
| tiktok | account, post, create-post |
| youtube | channel, video, upload |
| rednote | account, post, create-post |
What each one returns:
facebook post: Extract public profile details, highlighted information, photos, and posts with reaction counts, comment summaries, and media attachments.facebook post-detail: Extract one post from its permalink with its reactions breakdown, metrics, media, and comments.- Facebook creation: Prepare or publish Posts, Jobs, Live videos, Stories, Reels, Life updates, and Notes through the signed-in Chrome UI.
instagram profile: Extract a profile header plus its visible image, carousel, and reel grid.instagram post: Extract one image post, carousel, or reel with its caption, media, metrics, and visible comments.instagram create-post: Prepare or publish an image, carousel, or video through the signed-in web composer.youtube channel,youtube video,youtube upload: List a channel and videos, inspect one video, or prepare/publish a Studio upload.tiktok account,tiktok post,tiktok create-post: List an account, extract a video with direct media URLs, or upload through TikTok Studio.rednote account,rednote post,rednote create-post: List a creator and notes, extract image/video media and counts, or compose through Creator.x account: Export your own or any handle's profile, timeline posts, exact engagement counts, full post text, ISO dates, media, and replies.x post: Export one post from its status page with its thread context, exact metrics, media, and replies.x create-post: Prepare or publish posts with audience and reply controls plus images, GIFs, or video.
Every site runs on the browser transport with headless: false, so a run opens a visible Chrome window
using your signed-in session. Facebook and X both serve reduced markup to headless browsers, and the
creation flows are designed to leave a draft on screen for you to review, so a visible window is the
working configuration rather than a debugging aid.
Navigation goes through ctx.open(), website-api's single browser primitive: it navigates once and then
clears whatever is in the way — a login form, a one-time code prompt, a checkpoint — before the site reads
the page. No site here probes for a login screen itself.
Installation
pnpm add social-website-api website-apiInstall the complete pack through website-api and UNPKG
The published npm tarball contains a generated, self-contained website-api site pack plus an integrity catalog
under registry/. Configure that UNPKG directory as an HTTPS registry, then install the pack once:
npx website-api ext registry add \
https://unpkg.com/social-website-api@latest/registry
npx website-api ext install social-website-api --refresh
npx website-api listUse an exact version instead of @latest for a reproducible install:
npx website-api ext registry add \
https://unpkg.com/[email protected]/registryAfter installation, run any contributed site normally; the pack id is only used for install, update, and remove:
npx website-api instagram profile --username ecoldbrew_official --pages 1
npx website-api ext run --site instagram social-website-api -- \
profile --username ecoldbrew_official --pages 1
npx website-api ext update social-website-api
npx website-api ext remove social-website-apiext run refreshes and installs the pack, then runs the selected member immediately. --site may go before
or immediately after the pack id; options after -- are forwarded to that member.
This requires a website-api release with registry site-pack (siteIds) support. Older releases only validate
one site per catalog entry and will reject this pack.
Quick Start
import { createSocialWebsiteApi } from "social-website-api";
const api = createSocialWebsiteApi();
// Extract profile details and posts from Facebook
const result = await api.queryWebsite("facebook", {
operation: "post",
profile: "zuck",
pages: 5,
});
console.log(result);Instagram profile, post, and creation endpoints
instagram profile accepts a username, @username, or profile URL. It returns the rendered profile
header and de-duplicated posts from the grid, then visits each post for its caption, like/comment/repost
counts, media, and direct video URL. pages (1–30) is the maximum number of grid viewports to inspect and
postLimit (1–300) bounds the result. Set includeDetails: false only when fast grid previews are enough.
const profile = await api.queryWebsite("instagram", {
operation: "profile",
username: "ecoldbrew_official",
pages: 3,
postLimit: 30,
includeDetails: true,
});instagram post accepts a full Instagram URL, a /p/<shortcode> or /reel/<shortcode> path, or a bare
shortcode. The decoder scopes itself to the focused post, so Instagram's “More posts from” grid is not
mixed into the returned media or comments. Video posts expose Instagram's progressive MP4 as both
video_url and media[].url; these signed CDN URLs expire, so consume or download them promptly.
const post = await api.queryWebsite("instagram", {
operation: "post",
post: "https://www.instagram.com/ecoldbrew_official/reel/DX-xHe-NaOA/",
});The Instagram composer is safe by default: publish defaults to false, leaving the uploaded media and
caption on Instagram's final Share screen for review. It accepts up to ten JPG/JPEG/PNG/AVIF/HEIC/HEIF
images or MP4/MOV videos, including mixed carousels.
const draft = await api.queryWebsite("instagram", {
operation: "create-post",
media: "/absolute/path/photo.jpg,/absolute/path/clip.mp4",
caption: "A reviewable Instagram draft",
});
const published = await api.queryWebsite("instagram", {
operation: "create-post",
media: "/absolute/path/photo.jpg",
caption: "Ready to share",
publish: true,
});When served by website-api, the same operations are HTTP endpoints:
curl -H 'content-type: application/json' \
-d '{"username":"ecoldbrew_official","pages":2}' \
http://127.0.0.1:3000/v1/sites/instagram/profile
curl -H 'content-type: application/json' \
-d '{"media":"/absolute/path/photo.jpg","caption":"Review this draft"}' \
http://127.0.0.1:3000/v1/sites/instagram/create-postYouTube, TikTok, and Rednote
All write endpoints are review-first: publish defaults to false, uploads the local media, fills the form,
and leaves the browser open. Set publish: true only when the endpoint should press the final publish control.
Every read endpoint stops scrolling as soon as its limit is met, so asking for 5 posts costs 5 posts of
work rather than a crawl of the whole account. videos_complete / posts_complete say whether the account
was actually exhausted or the limit cut the list short.
// Opens YouTube's "More about this channel" dialog for channel_url, joined_date,
// total_views, subscribers_count, and videos_count, then scrolls the grid.
// Each video carries published_at plus the display_date the grid shows; the grid
// only ever states a relative date, so published_at derived from "6 months ago"
// is flagged published_approximate: true. A watch page states the exact date, so
// `youtube video` never approximates.
const channel = await api.queryWebsite("youtube", { operation: "channel", channel: "@handle", videoLimit: 20 });
const youtubeVideo = await api.queryWebsite("youtube", { operation: "video", video: "WPQEYJALiMU" });
const youtubeDraft = await api.queryWebsite("youtube", {
operation: "upload",
media: "/absolute/path/video.mp4", title: "My video", visibility: "private",
});
// TikTok must already be open on this exact profile with a visible post grid.
// The endpoint reads that manually refreshed tab in place; it never navigates or
// reloads it. Scrolling is also off when scrollPages is 0.
const tiktokAccount = await api.queryWebsite("tiktok", {
operation: "account",
account: "handle", postLimit: 10, scrollPages: 0, reuseTab: true,
});
// Open this exact post and its Comments panel manually first. The endpoint only
// reads the visible DOM; it does not click, scroll, navigate, or request comments.
const tiktokPost = await api.queryWebsite("tiktok", { operation: "post", post: "https://www.tiktok.com/@handle/video/123" });
const tiktokDraft = await api.queryWebsite("tiktok", {
operation: "create-post",
media: "/absolute/path/video.mp4", caption: "Review this",
});
// Scrolls until Rednote stops yielding notes, with the complete original card
// under posts[].source_data. A note URL is only valid with its signed xsec_token,
// so take post URLs from a profile export rather than pasting an old link.
const rednoteAccount = await api.queryWebsite("rednote", { operation: "account", account: "USER_ID", postLimit: 10 });
const rednotePost = await api.queryWebsite("rednote", {
operation: "post",
post: "https://www.rednote.com/explore/POST_ID?xsec_token=...&xsec_source=pc_user",
});
const rednoteDraft = await api.queryWebsite("rednote", {
operation: "create-post",
media: "/absolute/path/photo.jpg,/absolute/path/photo-2.jpg", title: "Title", description: "Body",
});TikTok's manually refreshed tab requirement
TikTok can serve an empty grid after CDP-driven navigation while the same URL works after a human address-bar
refresh. For that reason, tiktok account requires the exact requested profile to already be open with at
least one rendered post. It refuses an empty/wrong tab with instructions instead of navigating, reloading,
scrolling, or writing a misleading empty export. scrollPages defaults to the endpoint's normal pagination
limit, so pass scrollPages: 0 for strict DOM-only inspection; the live export script does this by default.
tiktok post follows the same rule. Manually open the exact video/photo and its Comments panel; it reads only
the post and comment rows already visible in the DOM and performs no interaction or comment API request.
npx tsx scripts/export-live.ts tiktok handles missing profile/post tabs automatically via CDP Runtime domain evaluation (Runtime.evaluate with window.location.href = targetUrl), navigating the browser tab natively before performing DOM-only extraction.
X account export and posting
x account accepts me, OpenAI, @OpenAI, or a profile URL. It is deliberately a single page load:
the profile is fetched once and every additional post comes from scrolling the timeline already on screen.
The returned page_loads says how many pages were actually fetched, and pages_inspected how many scroll
positions were decoded. Each post carries a 1-based page_number for the scroll page it was first seen on.
const account = await api.queryWebsite("x", {
operation: "account",
handle: "OpenAI",
pages: 5,
postLimit: 20,
});
const ownAccount = await api.queryWebsite("x", { operation: "account", handle: "me" });A profile export has no replies, and no untruncated text. Replies belong to a post, not to a profile, and
X only serves the full body and the exact engagement counts on a post's own page — fetching those would mean
one request per post. Use x post for a single post's full body, exact metrics, and comments.
What that means for the values:
textis exactly what the timeline rendered, so a long post ends mid-sentence.full_textis absent here rather than a copy of the truncated text, so you can always tell the two apart.- Engagement counts are whatever the timeline labelled, which may be abbreviated or missing.
- Mentions, hashtags, and cashtags are kept as written. A URL X displayed in full is kept as displayed and
also reported in
links; a URL X truncated with an ellipsis is replaced by itst.cohref so the text never carries a dead link. Entity links stay out oflinks—replying_toalready reports mentions. - Exact counts need an English UI. Engagement counts come from X's own accessibility labels, which are
English-only, so a Chrome session running X in another language returns them (and
posts_count) asundefinedrather than as approximations.followers_countandfollowing_countare different: they are the follow link's own text with an English suffix stripped, so on a translated session they come back as the raw localized label rather than asundefined. - Most video has no fetchable URL.
media[].urlis populated only when X exposes a real file. Most video is streamed over HLS, so expecturlto be absent and usepreview_url(the poster frame) for those items.
includeAbout adds one extra page load for https://x.com/<handle>/about, whose rows carry the account's
country, verification date, and username history. It defaults to false so the default really is one request.
pages (1–30) is a scroll budget: one page is one decode of what X has rendered, and each page past the
first scrolls one viewport first. Scrolling stops early at the end of a timeline, so pages_inspected reports
what was read rather than what was requested. postLimit (1–100) bounds the result. Promoted posts are
excluded.
Single post export
Each platform has an endpoint for one post, for when you already have a permalink and do not want to walk a profile to reach it.
x post accepts a status URL, a handle/status/id path, or a bare numeric id. A status page renders the
whole conversation, so the result separates it: post is the post the URL names, thread is the posts above
it in the same thread (oldest first), and comments are the replies below it. X renders an author's own
follow-on posts in the same position as replies, so a self-thread's later posts appear in comments with the
account's own handle — compare comments[].author.handle against post.author.handle if you need to tell
those apart from other people's replies.
const post = await api.queryWebsite("x", {
operation: "post",
post: "https://x.com/AnthropicAI/status/2082153297670992134",
commentsPages: 2,
commentLimit: 50,
});facebook post-detail accepts a post URL, a <profile>/posts/<id> path, or a bare post id. A signed-in
permalink opens the post in an overlay above the news feed, so the decoder scopes to that overlay — the
surrounding feed is never mixed into the post. commentsPages (1–10) scrolls the overlay for more comments,
and comments_pages_inspected reports how many pages were actually read. Comments come back in Facebook's
default "Most relevant" order; this endpoint does not re-sort them or expand "View all N replies".
const detail = await api.queryWebsite("facebook", {
operation: "post-detail",
post: "https://www.facebook.com/zuck/posts/pfbid08oPnRNf43oPd9BgCktkgBKRTzih",
commentsPages: 3,
});Facebook obfuscates post timestamps by shuffling their characters and restoring the reading order with flex
layout, and it seeds the list with absolutely positioned decoy characters. Both decoders undo this by sorting
the characters by their laid-out position, which is why date is a rendered label like 6h or
July 24 at 7:02 AM rather than a machine timestamp. Facebook also aims the timestamp anchor at the profile
rather than the post, so post_url is recovered from whichever permalink the post's markup exposes.
Shared links and video need the same kind of care, because Facebook does not put either in an href:
- A shared link is reported as
link_card, with thedomainandtitleFacebook rendered (for exampleWSJ.COMandOpinion | The AI Future Is for Everyone). Facebook usually rewrites the card'shrefto the profile plus a__cft__token and resolves the destination on click, solink_card.urlis filled only when the page exposed the real URL — directly, or wrapped inl.facebook.com/l.php?u=…, which is unwrapped for you. Every outbound URL found in the post also lands inlinks. The card is an attachment rather than part of the body, so it is not spliced intotext. video_urlis the watch or reel page, not a media file. Facebook streams video through MediaSource, sovideo.srcis empty and there are no<source>elements — no direct file URL exists to report. Usevideo_urlto open the video andvideo_thumbnail_urlfor the poster frame. A video inside a comment does not sethas_videoon the post.
The X composer is safe by default: publish defaults to false, leaving the populated composer open for
review. audience is Everyone or the exact name of a community available to the signed-in account.
replyAudience accepts Everyone, Accounts you follow, Accounts you follow and who they follow,
Only accounts you mention, or Verified accounts.
const draft = await api.queryWebsite("x", {
operation: "create-post",
text: "A reviewable draft prepared through social-website-api",
media: "/absolute/path/photo.jpg,/absolute/path/photo-2.png",
audience: "Everyone",
replyAudience: "Accounts you follow",
});X media accepts up to four JPG/JPEG/PNG/WebP images, one GIF, or one MP4/MOV video. A GIF or video cannot
be mixed with other media in the same post. Media paths and replyAudience are validated before Chrome is
opened, and a draft is only returned once X shows the attachment in the composer.
Facebook creation endpoints
Each creation flow is a separate website-api operation. Creation is safe by default: publish defaults to
false, leaving the populated composer open for review in Chrome. Set publish: true only when the content
should be shared immediately.
| Operation | Main inputs |
| --- | --- |
| facebook create-post | text/media, tags, feeling/activity, check-in, GIF, nonprofit, audience, AI label, publish |
| facebook create-job | job details, pay/location, benefits, optional cover, publish |
| facebook create-live-video | source, audience, publish |
| facebook create-story | exactly one of text or media, optional background, publish |
| facebook create-reel | video, optional caption, publish |
| facebook create-life-update | category, event, title, message, media, audience, publish |
| facebook create-note | text (maximum 60 characters), publish |
const draft = await api.queryWebsite("facebook", {
operation: "create-post",
message: "A draft prepared through social-website-api",
media: "/absolute/path/photo.jpg",
tagPeople: "Friend One, Friend Two",
checkIn: "New York, New York",
audience: "Only me",
aiLabel: true,
});
// Explicitly publish after the same inputs have been reviewed.
const published = await api.queryWebsite("facebook", {
operation: "create-note",
text: "Shipping something new today",
publish: true,
});When served by website-api, these are available as POST /v1/sites/<site-id>, for example:
curl -H 'content-type: application/json' \
-d '{"message":"Review this draft","audience":"Only me"}' \
http://127.0.0.1:3000/v1/sites/facebook/create-postmedia accepts one absolute path or comma/newline-separated paths where the flow allows multiple files.
Facebook post options
facebook create-post covers every attachment that Facebook actually keeps in the normal post composer:
media: Photo/video.tagPeople: one name or comma/newline-separated friend names.feeling, or the pairactivityType+activity.checkIn: location search text.gifQuerywith optional zero-basedgifIndex(a GIF and uploaded media are mutually exclusive).nonprofit: nonprofit search text for Raise money.audience:Public,Friends,Close friends, orOnly me.aiLabel:trueturns the AI label on;falseturns it off (default).
Facebook's Create job and Live video buttons leave the ordinary composer and open dedicated flows, so
they are represented by facebook create-job and facebook create-live-video. Marketplace jobs are always
public. Live video publishing additionally requires a usable webcam or an active streaming-software source;
with publish: false, the endpoint safely prepares Live Producer without starting a broadcast.
Modular Exports
You can also import individual sites directly:
import { facebookPostDetailSite, facebookPostSite } from "social-website-api/facebook";
import { instagramCreatePostSite, instagramPostSite, instagramProfileSite } from "social-website-api/instagram";
import { rednoteAccountSite, rednotePostSite } from "social-website-api/rednote";
import { tiktokAccountSite, tiktokPostSite } from "social-website-api/tiktok";
import { xAccountSite, xCreatePostSite, xPostSite } from "social-website-api/x";
import { youtubeChannelSite, youtubeVideoSite } from "social-website-api/youtube";Live export script
scripts/export-live.ts runs the read-only endpoints against the real sites and writes JSON to output/.
It is per site — a run visits only the sites you name, and naming none does nothing:
npx tsx scripts/export-live.ts instagramSites are x, facebook, instagram, youtube, tiktok, rednote, plus all if you really want every
one. Each site fetches a small sample (a handful of posts, then one detail page) and takes its detail URL
from its own profile export, so no permalink or signed token is hardcoded. Override a target with
X_HANDLE, FACEBOOK_PROFILE, INSTAGRAM_PROFILE, YOUTUBE_CHANNEL, TIKTOK_ACCOUNT, TIKTOK_POST, or
REDNOTE_ACCOUNT:
YOUTUBE_CHANNEL=@handle npx tsx scripts/export-live.ts youtubeLicense
MIT © guocity
