The simplest way to interact with Calendly's API v2 in Ruby. No runtime dependencies, no ceremony — just a Personal Access Token and you're good to go.
📖 Behind the gem: Curious about the design decisions, trade-offs, and real-world lessons from building and maintaining Calendlyr? I wrote a 5-part series covering everything — from why gems aren't as scary as you think to how this wrapper was built from scratch with zero dependencies.
Calendlyr requires Ruby >= 3.2.0.
Add to your Gemfile:
gem "calendlyr"Then run bundle install. That's it.
client = Calendlyr::Client.new(token: ENV["CALENDLY_TOKEN"])
# List your scheduled events — just pass the UUID, no full URI needed
events = client.events.list(user: "YOUR_USER_UUID")
events.data
#=> [#<Calendlyr::Event>, #<Calendlyr::Event>, ...]
# Access event details naturally
event = events.data.first
event.name #=> "30 Minute Meeting"
event.status #=> "active"
event.start_time #=> "2024-01-15T10:00:00.000000Z"
# List invitees for an event
invitees = client.events.list_invitees(uuid: event.uuid)
invitees.data.first.email #=> "[email protected]"For single-tenant apps, you can configure Calendlyr once and reuse a default client:
Calendlyr.configure do |config|
config.token = ENV.fetch("CALENDLY_TOKEN")
config.open_timeout = 5
config.read_timeout = 15
end
client = Calendlyr.client
events = client.events.list(user: "YOUR_USER_UUID")Calendlyr.client memoizes a client instance and rebuilds it if token or timeout values change.
Calendlyr can emit request lifecycle logs with any logger-like object that responds to info, debug, warn, and error. Logging is opt-in, and the gem does not ship a logger implementation for you.
require "logger"
client = Calendlyr::Client.new(token: ENV["CALENDLY_TOKEN"], logger: Logger.new($stdout))Logger is just an example. You can pass any object that responds to info, debug, warn, and error.
If you're on Ruby 4 and want to use Ruby's Logger, make sure your application includes the logger gem.
Or configure it globally:
Calendlyr.configure do |config|
config.token = ENV.fetch("CALENDLY_TOKEN")
config.logger = Logger.new($stdout)
endIn Rails, this is typically configured in an initializer:
# config/initializers/calendlyr.rb
Calendlyr.configure do |config|
config.token = ENV.fetch("CALENDLY_TOKEN")
endImportant
Calendlyr.client is module-global and not thread-safe for multi-tenant usage.
If your app serves multiple tenants or uses per-request credentials, use Calendlyr::Client.new(token:) per request.
Most methods that take a Calendly resource reference (like user:, organization:, event_type:, or owner:) accept both bare UUIDs and full URIs. When supported, the gem expands bare UUIDs automatically:
# Both are equivalent:
client.events.list(user: "YOUR_USER_UUID")
client.events.list(user: "https://api.calendly.com/users/YOUR_USER_UUID")The gem mirrors the Calendly API closely, so converting API examples into gem code is straightforward. Responses are wrapped in Ruby objects with dot-access for every field.
Note: A few endpoints still expect a full Calendly URI for specific parameters. When that matters, the resource docs call it out explicitly.
Calendlyr::Webhook (singular) verifies signed webhook payloads. This is separate from client.webhooks / Calendlyr::Webhooks (plural), which are API resources for managing webhook subscriptions.
- Calendly sends the signature in the
Calendly-Webhook-SignatureHTTP header. - In Rack/Rails, that header is available as
HTTP_CALENDLY_WEBHOOK_SIGNATURE. verify!raises on invalid signature/timestamp;valid?returnstrue/false;parseverifies first, then JSON-parses and wraps the payload.
payload = request.body.read
signature_header = request.get_header("HTTP_CALENDLY_WEBHOOK_SIGNATURE")
signing_key = ENV.fetch("CALENDLY_WEBHOOK_SIGNING_KEY")
if Calendlyr::Webhook.valid?(payload: payload, signature_header: signature_header, signing_key: signing_key)
webhook = Calendlyr::Webhook.parse(
payload: payload,
signature_header: signature_header,
signing_key: signing_key
)
webhook.event #=> "invitee.created"
webhook.payload #=> Calendlyr::Webhooks::InviteePayload (for invitee.* events)
#=> Calendlyr::Object (for other/unknown events)
endAll API objects support #to_json for easy serialization (caching, logging, API proxying):
event = client.events.retrieve(uuid: "ABC123")
event.to_json
#=> '{"uri":"https://api.calendly.com/scheduled_events/ABC123","name":"30 Minute Meeting",...}'
# Works with JSON.generate and nested objects
JSON.generate(event)
# Round-trip: parse back into an Object
parsed = Calendlyr::Object.new(JSON.parse(event.to_json))
parsed.name #=> "30 Minute Meeting"Note:
#to_jsonand#to_hexclude the internalclientreference — only API data is serialized.
API errors now include the HTTP method and path in the message, and expose structured attributes for debugging:
begin
client.events.retrieve(uuid: "INVALID_UUID")
rescue Calendlyr::NotFound => error
error.message #=> "[Error 404] GET /scheduled_events/INVALID_UUID — Not Found. The resource you requested does not exist."
error.status #=> 404
error.http_method #=> "GET"
error.path #=> "/scheduled_events/INVALID_UUID"
error.response_body #=> { "title" => "Not Found", "message" => "..." }
endThis makes debugging failed requests much easier without changing existing rescue Calendlyr::Error patterns.
Calendlyr supports lazy auto-pagination for paginated collection endpoints. There are two ways to consume paginated results:
The simplest option. Fetches every page and returns all items as an Array.
# Get all events across all pages (e.g., hundreds of events)
events = client.events.list_all(organization: "YOUR_ORG_UUID")
events #=> [#<Calendlyr::Event>, #<Calendlyr::Event>, ...]
# Same pattern works for every resource with a list method:
client.event_types.list_all(organization: "YOUR_ORG_UUID")
client.webhooks.list_all(organization: "YOUR_ORG_UUID", scope: "organization")
client.organizations.list_all_memberships(organization: "YOUR_ORG_UUID")
client.organizations.list_all_invitations(uuid: "YOUR_ORG_UUID")
client.organizations.list_all_activity_log(organization: "YOUR_ORG_UUID")
client.groups.list_all(organization: "YOUR_ORG_UUID")
client.groups.list_all_relationships(organization: "YOUR_ORG_UUID")
client.routing_forms.list_all(organization: "YOUR_ORG_UUID")
client.routing_forms.list_all_submissions(form: "YOUR_FORM_UUID")
client.availability.list_all_user_busy_times(user: "YOUR_USER_UUID", start_time: "...", end_time: "...")
client.availability.list_all_user_schedules(user: "YOUR_USER_UUID")
client.locations.list_all
client.event_types.list_all_memberships(event_type: "YOUR_EVENT_TYPE_UUID")
client.outgoing_communications.list_all(organization: "YOUR_ORG_UUID")Returns an Enumerator::Lazy that fetches pages on demand. Pages are only requested as you consume items, so you can stop early without fetching all pages.
collection = client.events.list(organization: "YOUR_ORG_UUID")
# Take only the first 50 events — fetches only as many pages as needed
first_50 = collection.auto_paginate.take(50)
# Filter lazily — stops fetching once the condition is met
active = collection.auto_paginate.select { |e| e.status == "active" }.first(10)
# Consume all items lazily
collection.auto_paginate.each do |event|
puts event.name
endFor the full list of available resources and methods, check out the API Reference.
The docs in this repository focus on the Ruby wrapper API. For request/response schemas and endpoint-level behavior, use the official Calendly API docs linked from each resource page.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
When adding resources, please write tests and update the docs.
