circle-so-sdk
v0.1.0
Published
Typed, zero-dependency client for the Circle.so Admin API v2 — members, access groups, spaces, space groups, token version detection, and real captured API fixtures for testing.
Maintainers
Readme
circle-so-sdk
Typed, zero-dependency client for the Circle.so Admin API v2 — members, access groups, spaces, space groups, token version detection — plus real captured API fixtures for testing your integration without hitting Circle.
Unofficial. Not affiliated with Circle Internet Services, Inc.
Install
npm install circle-so-sdkRequires Node.js ≥ 18 (uses the global fetch).
Quick start
import { createCircleClient, detectTokenVersion } from 'circle-so-sdk'
// Circle issues v1 (Professional plan) and v2 (Business plan) admin tokens
// that look identical. A v1 token on the v2 API returns a plain 401, so use
// the three-state probe to give users an actionable error:
const version = await detectTokenVersion(token) // 'v2' | 'v1' | 'invalid'
const circle = createCircleClient({ token })
const community = await circle.getCommunity()
// Invite (idempotent — re-inviting an existing email does not re-send):
const { member, alreadyExisted } = await circle.createOrInviteMember('[email protected]', 'User Name')
// Access groups (member identified by email, group id in the URL):
const groups = await circle.listAccessGroups()
await circle.addToAccessGroup(groupId, '[email protected]')
await circle.removeFromAccessGroup(groupId, '[email protected]')
// Spaces / space groups (id goes in the request BODY, even for DELETE):
await circle.addToSpace(spaceId, '[email protected]')
await circle.addToSpaceGroup(spaceGroupId, '[email protected]')Error handling
All non-OK responses throw CircleApiError with a classified kind:
| kind | meaning |
|---|---|
| unauthorized | 401 — wrong/invalid token (or a v1 token on the v2 API) |
| forbidden | 403 — plan no longer allows the endpoint (plan downgrade) |
| not_found | 404 with a JSON body — missing record/param |
| wrong_path | 404 with an HTML body — the route does not exist (bug guard) |
| rate_limited | 429 |
| server_error | 5xx |
import { CircleApiError } from 'circle-so-sdk'
try {
await circle.addToAccessGroup(groupId, email)
} catch (error) {
if (error instanceof CircleApiError && error.kind === 'unauthorized') {
// pause syncing, prompt the user to re-connect
}
}The wrong_path distinction matters: Circle returns 404 HTML for routes that don't exist and 404 JSON for missing records. The client tells them apart via Content-Type so a typo'd endpoint doesn't masquerade as "record not found".
API quirks this client encodes
Learned from a live integration (all fixtures under circle-so-sdk/mocks were captured against the real API):
- Asymmetric membership endpoints. Access groups take the group id in the URL and the member's email in the body (
POST /access_groups/:id/community_members). Spaces and space groups instead use flat endpoints (/space_members,/space_group_members) with the id in the body — even forDELETE. - Members are keyed by email, not member id, for all membership operations.
- Adds/removes are idempotent. Re-adding returns the same success response; you can apply a full expected set without querying current state first.
- Space group membership cascades to all spaces inside the group (
CircleSpace.space_grouplets you detect overlaps). - First
createOrInviteMembersends Circle's invitation email; subsequent calls for the same email are no-ops (alreadyExisted: true).
Testing your integration: circle-so-sdk/mocks
Real request/response pairs captured from the live Admin API, ready to feed into MSW:
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { CIRCLE_BASE_V2, circleScenarios, CIRCLE_WRONG_PATH_HTML } from 'circle-so-sdk/mocks'
const server = setupServer(
http.post(`${CIRCLE_BASE_V2}/community_members`, () =>
HttpResponse.json(circleScenarios.memberInvited.body, { status: 201 }),
),
http.post(`${CIRCLE_BASE_V2}/access_groups/:id/community_members`, () =>
HttpResponse.json(circleScenarios.accessGroupAdd.body, { status: 201 }),
),
)Each scenario carries method, path, requestBody, status, contentType, and the captured body, so you can also drive a generic handler loop or assert against the shapes directly.
Options
createCircleClient({
token, // required — Circle Admin API v2 token (Business plan+)
baseUrl, // optional — override https://app.circle.so/api/admin/v2 (testing)
fetch: customFetch, // optional — inject a fetch implementation
})
detectTokenVersion(token, { baseUrlV2, baseUrlV1, fetch }) // overrides optionalLicense
MIT
