circle-so
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.
Installation
gem install circle-so
Or in your Gemfile:
gem "circle-so"
Requires Ruby ≥ 3.1. No runtime dependencies (built on net/http).
Quick start
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.
Resources
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.
Error handling
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.}")
end
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".
Token version detection
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 :invalid
API quirks this client encodes
Learned 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.
Development
bundle install
bundle exec rake spec # runs the suite with 100% line-coverage enforcement
Tests stub Circle at the network boundary with WebMock, replaying real response bodies captured from the live Admin API — no credentials needed, fully deterministic.
License
MIT