Skip to content

Repository files navigation

circle-so-sdk

npm version publish license node dependencies

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-sdk

Requires 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('user@example.com', 'User Name')

// Access groups (member identified by email, group id in the URL):
const groups = await circle.listAccessGroups()
await circle.addToAccessGroup(groupId, 'user@example.com')
await circle.removeFromAccessGroup(groupId, 'user@example.com')

// Spaces / space groups (id goes in the request BODY, even for DELETE):
await circle.addToSpace(spaceId, 'user@example.com')
await circle.addToSpaceGroup(spaceGroupId, 'user@example.com')

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 for DELETE.
  • 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_group lets you detect overlaps).
  • First createOrInviteMember sends 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 optional

License

MIT

About

Typed, zero-dependency Node.js client for the Circle.so Admin API v2 — members, access groups, spaces, token detection, plus captured API fixtures for testing

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages