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:

ShapeResult hasNext pageUsed by
CursornextCursorpass it as cursorfeed, saved, likes
OffsetnextOffsetpass it as offsetprofile, 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:

KindExamplesLimit
PostscreatePost10 per 10 minutes, 100 per day
Reactionslikes, reposts, saves60 per minute, 2000 per day
Commentscomment20 per 10 minutes, 200 per day
Followsfollow, unfollow30 per 10 minutes, 300 per day
Safetyblock, mute, report30 per 10 minutes, 200 per day
ProfileupdateProfile, avatar, banner10 per hour, 30 per day
SettingsupdateSettings30 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 read

Direct 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 need offset. Check which one the result carries.
  • Looping on RateLimited. Wait retryAfterSeconds, once.
  • Assuming everyone sees the same thing. Results depend on who is signed in.
  • Building the request yourself. unet.api handles tokens, refresh, timeouts and errors; calling the API with your own fetch skips all of that.

What next