npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

social-website-api

v0.2.2

Published

Browser-backed website-api integrations for Facebook, Instagram, Rednote, TikTok, X, and YouTube.

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 flags

A 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-api

Install 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 list

Use an exact version instead of @latest for a reproducible install:

npx website-api ext registry add \
  https://unpkg.com/[email protected]/registry

After 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-api

ext 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-post

YouTube, 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:

  • text is exactly what the timeline rendered, so a long post ends mid-sentence. full_text is 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 its t.co href so the text never carries a dead link. Entity links stay out of linksreplying_to already 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) as undefined rather than as approximations. followers_count and following_count are 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 as undefined.
  • Most video has no fetchable URL. media[].url is populated only when X exposes a real file. Most video is streamed over HLS, so expect url to be absent and use preview_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 the domain and title Facebook rendered (for example WSJ.COM and Opinion | The AI Future Is for Everyone). Facebook usually rewrites the card's href to the profile plus a __cft__ token and resolves the destination on click, so link_card.url is filled only when the page exposed the real URL — directly, or wrapped in l.facebook.com/l.php?u=…, which is unwrapped for you. Every outbound URL found in the post also lands in links. The card is an attachment rather than part of the body, so it is not spliced into text.
  • video_url is the watch or reel page, not a media file. Facebook streams video through MediaSource, so video.src is empty and there are no <source> elements — no direct file URL exists to report. Use video_url to open the video and video_thumbnail_url for the poster frame. A video inside a comment does not set has_video on 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-post

media 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 pair activityType + activity.
  • checkIn: location search text.
  • gifQuery with optional zero-based gifIndex (a GIF and uploaded media are mutually exclusive).
  • nonprofit: nonprofit search text for Raise money.
  • audience: Public, Friends, Close friends, or Only me.
  • aiLabel: true turns the AI label on; false turns 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 instagram

Sites 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 youtube

License

MIT © guocity