Zero-dependency Ruby client for the Circle.so Admin API v2 — full coverage of all 68 endpoints (official docs): members, spaces, space groups, access groups, posts, comments, events, courses, segments, tags, and more, with classified errors and token version detection.
Unofficial. Not affiliated with Circle Internet Services, Inc.
gem install circle-soOr in your Gemfile:
gem "circle-so"Requires Ruby ≥ 3.1. No runtime dependencies (built on net/http).
require "circle_so"
client = CircleSo::Client.new(token: ENV["CIRCLE_SO_TOKEN"])
community = client.community.show
# Invite a member (idempotent — re-inviting an existing email does not
# create a duplicate or re-send the invitation email):
result = client.community_members.invite(email: "user@example.com", name: "User Name")
result["community_member"]["id"]
# Access groups (member identified by email, group id in the URL):
client.access_groups.list
client.access_groups.add_member(group_id, email: "user@example.com")
client.access_groups.remove_member(group_id, email: "user@example.com")
# Spaces / space groups (id goes in the request BODY, even for DELETE):
client.space_members.add(space_id: 123, email: "user@example.com")
client.space_group_members.add(space_group_id: 456, email: "user@example.com")
# Posts, events, courses, ...
client.posts.list(space_id: 123)
client.events.create(name: "Launch party", space_id: 123)
client.course_lessons.update_progress(lesson_id: 1, user_email: "user@example.com")All responses are parsed JSON (Hash/Array with string keys), exactly as Circle returns them.
Every Admin API v2 endpoint is covered:
client. … |
Methods |
|---|---|
access_groups |
list, create, update, archive, unarchive, members, member, add_member, remove_member |
chat_preferences |
update |
comments |
list, create, show, delete |
community |
show, update |
community_members |
list, search, show, update, invite/create, deactivate, delete, ban, access_groups, spaces |
community_segments |
list, create, update, delete, duplicate |
course_lessons |
list, create, show, update, delete, update_progress |
course_sections |
list, create, show, update, delete |
direct_uploads |
create |
embeds |
create, show |
event_attendees |
list, add, remove |
events |
list, create, show, update, delete, duplicate |
flagged_contents |
list, create |
forms |
list, show, update, delete, duplicate |
gamification |
leaderboard |
images |
create_post, delete_post, duplicate_post |
invitation_links |
list, delete, revoke |
member_tags |
list, create, show, update, delete |
messages |
create |
page_profile_fields |
list |
posts |
list, create, show, update, delete, summary, unfollow |
profile_fields |
list, archive, unarchive |
search |
advanced |
space_group_members |
list, show, add, remove |
space_groups |
list, create, show, update, delete |
space_members |
list, show, add, remove |
spaces |
list, create, show, update, delete, ai_summaries |
tagged_members |
list, show, add, remove |
topics |
list, create, show, update, delete |
List endpoints accept query params as keyword arguments (page:, per_page:, filters). Create/update endpoints accept the request body as keyword arguments. There is also a low-level escape hatch: client.get(path, params), client.post(path, body), etc.
Non-2xx responses raise a subclass of CircleSo::APIError with a classified kind, the HTTP status, and the parsed response body:
| class | kind | meaning |
|---|---|---|
CircleSo::UnauthorizedError |
:unauthorized |
401 — wrong/invalid token (or a v1 token on the v2 API) |
CircleSo::ForbiddenError |
:forbidden |
403 — plan no longer allows the endpoint |
CircleSo::NotFoundError |
:not_found |
404 with a JSON body — missing record/param |
CircleSo::WrongPathError |
:wrong_path |
404 with an HTML body — the route does not exist (bug guard) |
CircleSo::RateLimitedError |
:rate_limited |
429 |
CircleSo::ServerError |
:server_error |
5xx |
begin
client.access_groups.add_member(group_id, email: email)
rescue CircleSo::UnauthorizedError
# pause syncing, prompt the user to re-connect
rescue CircleSo::APIError => e
logger.error("Circle #{e.status} (#{e.kind}): #{e.message}")
endThe 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".
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:
CircleSo.detect_token_version(token) # => :v2, :v1, or :invalidLearned from a live integration (the test fixtures under spec/fixtures 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.
- First
invitesends Circle's invitation email; subsequent calls for the same email are no-ops (the response message says "already a member"). course_lessonsupdate usesPATCHwhile every other update usesPUT;tagged_members.removeidentifies the record via query params.
bundle install
bundle exec rake spec # runs the suite with 100% line-coverage enforcementTests stub Circle at the network boundary with WebMock, replaying real response bodies captured from the live Admin API — no credentials needed, fully deterministic.
MIT