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.message}")
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 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.
  • First invite sends Circle's invitation email; subsequent calls for the same email are no-ops (the response message says "already a member").
  • course_lessons update uses PATCH while every other update uses PUT; tagged_members.remove identifies 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