Friends
Reading the social graph, pending request inboxes, typed events with offline replay, and the refresh cadence consumers must drive themselves.
client.friends exposes the social graph as a snapshot, individual reads, mutations, and a typed event bus. The full surface is in the API reference; this page covers the consumer-facing patterns you'll hit immediately.
Reads
const snap = await client.friends.snapshot();
console.log(`mutuals=${snap.mutuals.length} received=${snap.received.length} sent=${snap.sent.length}`);FriendsSnapshot is the single source of truth — list(), receivedRequests(), and sentRequests() all project off it.
const friends = await client.friends.list(); // mutuals only
const received = await client.friends.receivedRequests();
const sent = await client.friends.sentRequests();Friend.username is sometimes empty when the bundle's publicUsers cache hasn't been populated yet for that id. Match by userId and backfill when needed:
const ids = friends.filter((f) => !f.username).map((f) => f.userId);
const resolved = await client.friends.getUsers(ids);getUsers() is cache-first — only IDs that miss the bundle's publicUsers cache hit GetSnapchatterPublicInfo. Pass { refresh: true } to force a fresh RPC for every id. Server-confirmed misses (deleted accounts, blocks) come back with notFound: true.
Mutations
import { FriendSource } from "@snapcap/native";
await client.friends.sendRequest(userId); // default: ADDED_BY_USERNAME
await client.friends.sendRequest(userId, { source: FriendSource.ADDED_BY_SEARCH });
await client.friends.acceptRequest(req.fromUserId);
await client.friends.rejectRequest(req.fromUserId);
await client.friends.block(userId);
await client.friends.unblock(userId);client.friends.remove(userId) is a no-op against web.snapchat.com. The RPC reaches the server (HTTP 200, gRPC status 0), but the friendship is not severed — Snap restricts unfriend at the policy layer to mobile clients. The method is kept on the interface for symmetry; production logic should not depend on it. To actually unfriend an account, the user must do it from the official mobile app.
Typed events
const sub = client.friends.on("request:received", (req) => {
console.log(`new request from ${req.fromUsername}`);
});
client.friends.on("friend:added", (friend) => console.log(`+ ${friend.username}`));
client.friends.on("friend:removed", (userId) => console.log(`- ${userId}`));
client.friends.on("change", (snap) => console.log(`graph changed (${snap.mutuals.length} mutuals)`));The full event map is in FriendsEvents. Each on(...) returns a Subscription — a callable unsubscribe with a .signal you can chain into other lifetimes:
const ctrl = new AbortController();
client.friends.on("request:received", onReq, { signal: ctrl.signal });
client.friends.on("change", onChange, { signal: ctrl.signal });
ctrl.abort(); // tears down bothYou must drive the refresh cadence
The bundle does not auto-poll for fresh state — the SPA's React layer normally drives that, and the SDK doesn't load React. So events like request:received and friend:added fire when the bundle's user-slice changes, but the user-slice doesn't change unless something pulls fresh data from Snap.
// Drive the cadence yourself — every 30s for inbox-style monitoring:
setInterval(() => client.friends.refresh(), 30_000);
client.friends.on("request:received", (req) => { /* ... */ });refresh() cascades through Snap's own syncFriends (one wire call drives both SyncFriendData for mutuals + outgoing AND IncomingFriendSync for inbound requests). Calling either endpoint yourself on top of refresh() is redundant and races the bundle's delta-token bookkeeping.
Offline replay
The five diff-style events (friend:added, friend:removed, request:received, request:cancelled, request:accepted) are powered by a watcher that diffs the live graph against a snapshot persisted in the DataStore. Deltas that happened while the SDK was offline replay on the next state tick after startup.
What this means in practice:
- Subscribers should be idempotent over redeliveries. Treat the event as "this id is now in / out of slot X" rather than "this just happened on the wire".
- The watcher is install-once-per-instance: it lives for the lifetime of the
Friendsmanager even after every subscriber unsubscribes, so the persisted snapshot stays fresh and a future subscriber doesn't replay the entire interim window as "new" deltas. - First-ever boot (no cached snapshot) seeds silently from the live slice — no replay storm on a fresh DataStore.
onChange vs on("change", ...)
Both surfaces fan out the full FriendsSnapshot on every graph mutation. onChange(cb) returns a plain unsubscribe thunk; on("change", cb) returns the same Subscription shape as the other typed events. Use whichever fits your callsite — they share the underlying bridge.
Search
const hits = await client.friends.search("alice");Returns matching User records with just userId / username / displayName populated — Snap's search index doesn't return the richer flags. Run the hits through getUsers() if you need bitmoji or profile-tier data.