Type-safe TypeScript SDK for Circle. Ships ESM-first with CJS fallback and runs on Node 18+.
- Strongly typed requests and responses from the official OpenAPI specs
- Admin, Headless, and Headless Auth surfaces in one SDK
- Smart fetch with timeouts, retries, and safe error parsing
- Optional headless token auto-refresh with single-flight behavior
- ESM and CJS builds with subpath exports
- Node 18+
- Admin API usage must run on a server runtime (see browser caveats below)
npm install circlesocircleso- shared helpers and the unifiedCircleSoclientcircleso/admin- Admin API v2circleso/headless- Headless Client API v1circleso/auth- Headless Auth API v1
import { CircleSo } from "circleso";
const adminToken = process.env.CIRCLE_ADMIN_TOKEN;
const headlessToken = process.env.CIRCLE_HEADLESS_TOKEN;
const refreshToken = process.env.CIRCLE_HEADLESS_REFRESH_TOKEN;
if (!adminToken || !headlessToken || !refreshToken) {
throw new Error(
"Missing CIRCLE_ADMIN_TOKEN, CIRCLE_HEADLESS_TOKEN, or CIRCLE_HEADLESS_REFRESH_TOKEN.",
);
}
const sdk = new CircleSo({
communityHost: process.env.CIRCLE_COMMUNITY_HOST,
admin: { token: adminToken },
headless: {
accessToken: headlessToken,
refreshToken,
autoRefresh: true,
},
auth: { token: refreshToken },
timeoutMs: 10_000,
retries: { retries: 2, baseDelayMs: 200, maxDelayMs: 2000 },
});
const posts = await sdk.admin.posts.list({ page: 1, per_page: 20 });
console.log(posts?.records ?? []);import { createAdmin } from "circleso/admin";
const adminToken = process.env.CIRCLE_ADMIN_TOKEN;
if (!adminToken) {
throw new Error("Missing CIRCLE_ADMIN_TOKEN.");
}
const admin = createAdmin({
token: adminToken,
communityHost: process.env.CIRCLE_COMMUNITY_HOST,
});
const data = await admin.accessGroups.list({
page: 1,
per_page: 50,
});
console.log(data?.records ?? []);Need the low-level OpenAPI client? Use admin.raw or createAdminClient.
import { createHeadless } from "circleso/headless";
const headlessToken = process.env.CIRCLE_HEADLESS_TOKEN;
if (!headlessToken) {
throw new Error("Missing CIRCLE_HEADLESS_TOKEN.");
}
const headless = createHeadless({
accessToken: headlessToken,
});
const data = await headless.bookmarks.list({
page: 1,
per_page: 20,
});
console.log(data?.records ?? []);Optional auto-refresh when tokens expire:
const headlessToken = process.env.CIRCLE_HEADLESS_TOKEN;
const refreshToken = process.env.CIRCLE_HEADLESS_REFRESH_TOKEN;
if (!headlessToken || !refreshToken) {
throw new Error("Missing CIRCLE_HEADLESS_TOKEN or CIRCLE_HEADLESS_REFRESH_TOKEN.");
}
const headless = createHeadless({
accessToken: headlessToken,
refreshToken,
autoRefresh: true,
});Need the low-level OpenAPI client? Use headless.raw or createHeadlessClient.
import { createAuth } from "circleso/auth";
const refreshToken = process.env.CIRCLE_HEADLESS_REFRESH_TOKEN;
if (!refreshToken) {
throw new Error("Missing CIRCLE_HEADLESS_REFRESH_TOKEN.");
}
const auth = createAuth({
token: refreshToken,
});
const data = await auth.accessToken.refresh({
refresh_token: refreshToken,
});
console.log(data?.access_token);Need the low-level OpenAPI client? Use auth.raw or createHeadlessAuthClient.
Default base URL for all clients is https://app.circle.so. communityHost is optional and defaults to
app.circle.so if omitted.
Common options across all clients:
timeoutMs: request timeout in millisecondsretries:{ retries, baseDelayMs, maxDelayMs, jitterRatio, retryNonIdempotent }userAgent: custom user-agent valuelogger: hooks for request/response logging with redactionfetcher: custom fetch implementation
Per-surface options:
admin:token,communityHost,baseUrlheadless:accessTokenortoken,refreshToken,autoRefresh,authBaseUrl,communityHostauth:token,communityHost,baseUrl
Note: the unified CircleSo class constructs all three surfaces, so it requires
an Admin token, Headless token, and Headless refresh token. If you only need
one surface, use createAdmin, createHeadless, or createAuth directly.
The Admin API requires a host header with your community domain. Browsers cannot set the Host header,
so Admin API calls must be made from a server runtime or a proxy you control. If communityHost is omitted,
it defaults to app.circle.so.
The low-level fetch utilities throw rich error objects when HTTP responses are not OK. Catch
CircleHttpError to inspect status, request ID, and error bodies.
import { CircleHttpError, createSmartFetch } from "circleso";
const smartFetch = createSmartFetch({ baseUrl: "https://api-headless.circle.so" });
try {
await smartFetch("/api/headless/v1/bookmarks");
} catch (error) {
if (error instanceof CircleHttpError) {
console.error(error.status, error.url);
console.error(error.requestId);
console.error(error.bodyText ?? error.parsedJson);
} else {
throw error;
}
}Use the pagination helpers directly, or the headless iterator helpers for common flows.
import { paginateOffset, toAsyncIterator } from "circleso";
import { createAdmin } from "circleso/admin";
const adminToken = process.env.CIRCLE_ADMIN_TOKEN;
if (!adminToken) {
throw new Error("Missing CIRCLE_ADMIN_TOKEN.");
}
const admin = createAdmin({
token: adminToken,
communityHost: process.env.CIRCLE_COMMUNITY_HOST,
});
const pages = paginateOffset({
perPage: 50,
maxItems: 120,
fetchPage: async ({ page, perPage }) => {
const data = await admin.accessGroups.list({
page,
per_page: perPage,
});
return data ?? { records: [], has_next_page: false };
},
});
for await (const accessGroup of toAsyncIterator(pages)) {
console.log(accessGroup.id, accessGroup.name);
}import { createHeadless } from "circleso/headless";
const headlessToken = process.env.CIRCLE_HEADLESS_TOKEN;
const chatRoomUuid = process.env.CIRCLE_CHAT_ROOM_UUID;
if (!headlessToken || !chatRoomUuid) {
throw new Error("Missing CIRCLE_HEADLESS_TOKEN or CIRCLE_CHAT_ROOM_UUID.");
}
const headless = createHeadless({
accessToken: headlessToken,
});
for await (const msg of headless.iter.chatRoomMessages(chatRoomUuid, {
direction: "previous",
maxItems: 100,
})) {
console.log(msg.id, msg.body);
}Provide logger hooks to capture requests and responses. Sensitive headers and token-like fields
are redacted by default.
import { createAdmin } from "circleso/admin";
const admin = createAdmin({
token: process.env.CIRCLE_ADMIN_TOKEN ?? "",
logger: {
onRequest: ({ request }) => console.log(request.method, request.url),
onResponse: ({ response }) => console.log(response.status),
},
});Runnable scripts are in examples/:
npx tsx examples/admin-list-something.ts
npx tsx examples/headless-create-bookmark.ts
npx tsx examples/refresh-token.tsRequired environment variables:
- Admin example:
CIRCLE_ADMIN_TOKEN, optionalCIRCLE_COMMUNITY_HOST(defaults toapp.circle.so) - Headless bookmark example:
CIRCLE_HEADLESS_TOKEN,CIRCLE_BOOKMARK_RECORD_ID, optionalCIRCLE_BOOKMARK_TYPE - Refresh example:
CIRCLE_HEADLESS_REFRESH_TOKEN
Run the sync and generation scripts together:
npm run spec:sync && npm run gen:typesMIT. See LICENSE.