Call the UNET API
Use unet.api to read and write as the signed-in person. Permissions, paging, cancelling, timeouts and retries, rate limits, and a tour of the areas with an example each.
unet.api is a typed client for the UNET REST API. It acts as the signed-in person: it attaches their access token, refreshes it, and retries once when the token has expired. This page is for UNET clients, apps that read and write inside UNET. Each call needs a permission; see Permissions.
The basics
const page = await unet.api.feed({ limit: 20 });
for (const post of page.posts) {
console.log(`@${post.author.username}: ${post.content}`);
}Every method returns plain data, typed. A visitor-open read (a public post or profile) works without a session; everything else needs sign-in. A 404 is thrown as ApiError with status: 404.
Results are relative to the signed-in person: blocks, private accounts they follow, and their content settings apply, exactly as in the UNET apps. Two people calling the same method can get different results.
Paging
Lists come in two shapes. The result tells you which one it uses:
| Shape | Result has | Next page | Used by |
|---|---|---|---|
| Cursor | nextCursor | pass it as cursor | feed, saved, likes |
| Offset | nextOffset | pass it as offset | profile, followers, following, comments, replies, notifications, blocks, and most others |
When nextCursor or nextOffset is missing or null, there is no next page.
const first = await unet.api.feed({ limit: 20 });
const second = await unet.api.feed({ limit: 20, cursor: first.nextCursor });paginate() walks the pages for you with an async generator. It stops when the result has no next position.
import { paginate } from "@unet/id-web";
for await (const page of paginate((p) => unet.api.feed({ cursor: p.cursor }), { maxPages: 5 })) {
for (const post of page.posts) console.log(post.id);
}
for await (const page of paginate((p) => unet.api.followers("alice", { offset: p.offset }))) {
console.log(page.users.length);
}Always pass maxPages when you do not intend to read everything.
Cancelling
withSignal() returns the same API with every request tied to an AbortSignal. Use it when the person navigates away.
const ctl = new AbortController();
const pending = unet.api.withSignal(ctl.signal).feed();
ctl.abort(); // pending rejects with the browser's AbortError, not NetworkError
await pending.catch(() => undefined);Timeouts and retries
Set these when you create the client:
import { UnetId } from "@unet/id-web";
const unet = new UnetId({
clientId: "unet_xxx",
redirectUri: `${location.origin}/auth/callback`,
timeoutMs: 30_000, // JSON requests; default 60 000, 0 = never. File uploads and streams have no timeout.
retries: 2, // default 0 (off)
maxRetryDelayMs: 10_000, // upper bound of one wait; default 30 000
});With retries above zero, a 429 is retried for any call (it was not processed), waiting as long as Retry-After says. A 5xx is retried only for GET calls. When the retries run out, the error is thrown as usual. Calls that create something are never retried after a server error, so a retry cannot post twice.
Idempotency
Creating a post sends an Idempotency-Key, generated for you. If you retry the same action yourself, for example after a network error, pass the same key so UNET can recognise it and never create the post twice:
const key = crypto.randomUUID(); // one per user action, kept across your retries
await unet.api.createPost({ content: "Hello from my app", idempotencyKey: key });Comments get an Idempotency-Key too. Likes, saves and follows are idempotent by nature: doing them twice leaves them done.
Rate limits
UNET limits requests per app and per person. Reads share one limit (120 requests a minute); writes are limited harder and by kind:
| Kind | Examples | Limit |
|---|---|---|
| Posts | createPost | 10 per 10 minutes, 100 per day |
| Reactions | likes, reposts, saves | 60 per minute, 2000 per day |
| Comments | comment | 20 per 10 minutes, 200 per day |
| Follows | follow, unfollow | 30 per 10 minutes, 300 per day |
| Safety | block, mute, report | 30 per 10 minutes, 200 per day |
| Profile | updateProfile, avatar, banner | 10 per hour, 30 per day |
| Settings | updateSettings | 30 per 10 minutes, 300 per day |
These numbers are set on the server and can change. What stays true is the behaviour: when you hit one, the call throws RateLimited with retryAfterSeconds (or null when unknown). Wait that long, and show the person a message instead of retrying in a loop.
import { RateLimited } from "@unet/id-web";
try {
await unet.api.like(123);
} catch (e) {
if (e instanceof RateLimited) {
console.log(`Slow down. Try again in ${e.retryAfterSeconds ?? 30} seconds.`);
} else {
throw e;
}
}A tour of the areas
Each example needs the permission named beside it. The full list of methods is in the generated reference.
Feed and posts (content:read)
const feed = await unet.api.feed({ type: "following" }); // "global" (default), "following" or "foryou"
const post = await unet.api.post(123); // by numeric id or by hashId from a post link
console.log(post.counts?.likes, post.viewer?.liked);Profiles and people (content:read)
const { profile, posts, nextOffset } = await unet.api.profile("@alice", { limit: 20 });
const { users } = await unet.api.followers("alice");
console.log(profile.displayName, posts.length, users.length, nextOffset);Search (search:read)
const hits = await unet.api.search("unet"); // { users, posts, hashtags, topics }
console.log(hits.users.length, hits.posts.length);Comments and reactions (content:read to read, posts:interact to write)
const { comments } = await unet.api.comments(123, { sort: "newest" });
await unet.api.like(123);
await unet.api.comment(123, { content: "Nice one", parentId: comments[0]?.id });Notifications (notifications:read)
const n = await unet.api.notifications({ unreadOnly: true, limit: 50 });
console.log(n.unreadCount);
await unet.api.markNotificationsRead(); // no id: marks all as readDirect messages and account or security notices are never included.
Library (library:read, library:write)
await unet.api.save(123);
const saved = await unet.api.saved({ limit: 20 }); // { posts, nextCursor }
console.log(saved.posts.length);Follows (follows:write)
const state = await unet.api.follow("alice"); // { following, pending }: pending for a private account
console.log(state.pending ? "Request sent" : "Following");Safety (safety:manage)
await unet.api.block("spammer");
await unet.api.mute("loud");
await unet.api.report({ targetType: "post", targetId: 9, reason: "spam" });Profile and settings (profile:write, settings:read, settings:write)
await unet.api.updateProfile({ bio: "Hi", links: [{ title: "Site", url: "https://example.com" }] });
const settings = await unet.api.settings();
await unet.api.updateSettings({ notifications: { likes: false } }); // send only what changes
console.log(settings);Posting with pictures, voice, video and audio is covered in Media, and live updates in Realtime.
Common mistakes
- Paging with the wrong parameter. Cursor lists need
cursor, offset lists needoffset. Check which one the result carries. - Looping on
RateLimited. WaitretryAfterSeconds, once. - Assuming everyone sees the same thing. Results depend on who is signed in.
- Building the request yourself.
unet.apihandles tokens, refresh, timeouts and errors; calling the API with your ownfetchskips all of that.