Skip to content
Social SDK Cloud, the hosted SDK, is taking early-access signups.Join the list
Social SDK megaphone markSocial SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Social graph

Read profiles, followers, and following lists, then follow, block, or mute accounts through the normalized graph API or each platform's native module.

Use social.graph when you need a provider-neutral profile or relationship operation. The facade is capability checked: it does not turn a provider’s feed or native response into a relationship that the adapter did not declare.

const profile = await social.graph.getProfile(account, { handle: "someone" });
const page = await social.graph.listRelationships(account, { kind: "followers", limit: 50 });
await social.graph.follow(profile.ref);

getProfile returns a ProfileRecord with a stable ref plus optional displayName, handle, avatarUrl, bio, and provider data in native. listRelationships returns Page<RelationshipRecord>: items contain a profile reference and relationship (following, follower, blocked, or muted), and nextCursor is an opaque provider cursor. Pass that cursor to another call with the same account and kind; the facade has no graph.iterate(). The mutation methods are follow, unfollow, mute, unmute, block, and unblock.

Setup

Configure one adapter and one connected-account reference on your server. The platform tabs below reuse social and account and show only what differs.

const social = createSocial({
  backend: x({ auth: { userId, accessToken }, appBearerToken: process.env.X_APP_BEARER_TOKEN }),
});
const account = connectedAccountRef({ backend: "default", platform: "x", accountId: userId });

Native calls take the guarded native module and an operation context:

const native = social.native("default", { acknowledgeUnsafe: true });
if (native === undefined) throw new Error("This adapter has no native module");

const context: AdapterOperationContext = {
  backendInstance: "default",
  correlationId: "graph",
  retryBudget: { maxAttempts: 2, maxElapsedMs: 30_000 },
};

Platform support

Adapters without a tab here do not implement this facade. The client rejects an unavailable operation with SocialError code unsupported_capability before adapter I/O.

X

Supported: profile; followers, following, blocked, and muted lists; follow, unfollow, mute, unmute, block, and unblock. Scopes: users.read, follows.read/follows.write, mute.read/mute.write, block.read/block.write. Block and unblock are Enterprise-plan only.

Configure x() with the connected user’s ID and, for user-context operations, an access token. appBearerToken is a separate app-only credential. Profile lookups and follower and following reads can use it when auth.accessToken is absent. Blocked and muted lists and every mutation need a user token with the scopes above and fail with missing_permission without one.

const followers = await social.graph.listRelationships(account, { kind: "followers", limit: 100 });

Profile records normalize the user ID, name, username, and description, with the validated user object under native. getProfile reads one user per call. Relationship pages keep X user IDs as profile refs and do not invent a since timestamp, and accept a limit of 1 to 100; the SDK does not expose X’s larger page sizes. An invalid size raises invalid_input. A revoked or expired user token raises reconnect_required.

X lists are native only: owned lists, list members, list following, and pinned lists. Pinned reads need users.read and list.read. X returns all pinned lists in one response, so the method takes no cursor or limit:

const pinned = await native.pinnedLists({ account, context });

Bluesky

Supported: profile; followers, following, blocked, and muted lists; all six mutations. Access: repo, through an access JWT or OAuth session.

Configure bluesky() with the PDS service, the account DID, and either an accessJwt or an OAuth session, not both. Profile lookup accepts handles and DIDs; normalized profile refs use the DID.

const adapter = bluesky({ auth: { service: "https://bsky.social", did, accessJwt } });

Records normalize DID, handle, display name, avatar, and description. Relationship reads map app.bsky.graph.getFollowers, getFollows, getMutes, and getBlocks, and cursors pass through unchanged. unfollow first reads viewer.following and unblock reads viewer.blocking; if that URI is absent the call fails with invalid_input. Mute and unmute use actor DIDs.

Bluesky lists are native only and need repo. The native surface covers list creation, member changes, list reads, and list mute and block:

const lists = await native.getLists({ account, actor: did, limit: 50, context });

Threads

Supported: profile only. Scope: threads_basic for profiles.read.

Configure the adapter with the authorized Threads user ID and access token. Handle lookup also depends on Threads profile discovery (threads_profile_discovery). Under Standard Access, Meta limits discovery to a small set of official accounts until your app has Advanced Access; see Threads profiles.

const adapter = threads({ auth: { userId, accessToken } });

For the authorized user, the adapter returns the profile ID, username, name, profile picture, biography, and verification data where present.

Handle lookup calls GET /profile_lookup with an exact username and requests only username, name, profile_picture_url, biography, and is_verified. Meta returns only public profiles with at least 100 followers. The response has no user ID, so the record’s ref.profileId is the synthetic value lookup:<username> and native._profileIdUnavailable is true. That ID is not a Threads user ID; do not pass it to other Threads calls.

Threads has no endpoint that lists followers or followed accounts, so the manifest declares graph.read as unsupported-by-platform, and listRelationships raises unsupported_capability before any request. Mutations raise the same error. For the authorized user’s total follower count, read the followers_count metric with social.analytics.getAccountMetrics.

Instagram

Supported: profile only. Scopes: instagram_business_basic and instagram_business_manage_insights, on a professional account.

instagram() uses Instagram Login by default. The scopes depend on the login flavor:

  • Instagram Login (default): instagram_business_basic and instagram_business_manage_insights.
  • Facebook Login (auth.flavor: "facebook-login"): instagram_basic, instagram_manage_insights, and pages_read_engagement.
const adapter = instagram({ auth: { accessToken, accountId } });
const own = await social.graph.getProfile(account, {});

getProfile accepts three selectors. {} reads the connected account. { profileId } reads an Instagram user ID your token can access. { handle } uses business discovery to read another professional account by username; it needs Facebook Login and raises unsupported_capability under Instagram Login. A profile record contains the normalized ID, username, name, biography, and profile image, with provider fields in native. Relationship reads and mutations raise unsupported_capability. See Meta’s business discovery guide for eligibility.

YouTube

Supported: no normalized graph. Subscriptions are native. Listing needs https://www.googleapis.com/auth/youtube.readonly; subscribing and unsubscribing need https://www.googleapis.com/auth/youtube or youtube.force-ssl.

Read or change subscriptions through the native adapter. The response is provider JSON, including nextPageToken when more results are available. Without channelId, list returns the authorized user’s own subscriptions:

const mine = await native.subscriptions({ action: "list", context });

Passing channelId lists the channels that channel subscribes to, which YouTube returns only when that channel’s subscriptions are public:

const theirs = await native.subscriptions({ action: "list", channelId: "UC_other", context });

Profile updates

Profile writes are native only, because each platform accepts different fields. Check the profile.update declaration in the capability matrix before you offer the feature.

  • Bluesky: native.updateProfile({ account, profile, context }) replaces the app.bsky.actor.profile record, so send the complete record.
  • YouTube: native.updateProfile({ part, value, context }) updates the channel’s brandingSettings.channel fields or localizations. See the YouTube guide.
  • LinkedIn: member profile writes use the Profile Edit API, which LinkedIn limits to approved developers. The adapter does not implement it.
  • X, Threads, Instagram, and TikTok: the platform APIs have no profile write, and the operation is declared unsupported-by-platform.

Errors and authorization

Catch SocialError at the application boundary and branch on code, not provider message text. Useful codes here are unsupported_capability, missing_permission, reconnect_required, unauthorized, invalid_input, not_found, rate_limited, and upstream_failure. Treat an ambiguous_outcome as uncertain rather than claiming that a follow, mute, or block succeeded. Native calls bypass tenant authorization and concurrency middleware.

Last updated on October 5, 2026