Ruby SDK
The Mobiscroll Connect Ruby SDK provides a convenient way to integrate Mobiscroll Connect in Ruby backend applications. It requires Ruby 3.2 or higher and is built on Faraday, so it works in any Ruby application (plain Rack, Sinatra, Rails, Hanami, etc.).
Setup
Install the gem with Bundler:
- Bundler
- gem
Add to your Gemfile:
gem 'mobiscroll-connect', '~> 1.0'
Then run:
bundle install
gem install mobiscroll-connect
Then require it:
require 'mobiscroll-connect'
Client Initialization
To use the SDK, initialize Mobiscroll::Connect::Client with your client credentials.
Class: Mobiscroll::Connect::Client
Constructor keyword arguments.
Your Client ID obtained from the Mobiscroll Connect dashboard.
Your Client Secret obtained from the Mobiscroll Connect dashboard.
Your application's redirect URI that matches the one configured in the Mobiscroll Connect dashboard.
Override the API base URL. Defaults to https://connect.mobiscroll.com/api.
HTTP timeout in seconds. Defaults to 30.
A pinned whpk_ public key for webhooks.verify_webhook. It is used only when the keys endpoint cannot be reached, and it stops verifying once Mobiscroll retires that key, so replace it on every key rotation. Leave it unset unless your handler cannot make outbound requests.
Usage:
require 'mobiscroll-connect'
client = Mobiscroll::Connect::Client.new(
client_id: ENV['MOBISCROLL_CLIENT_ID'],
client_secret: ENV['MOBISCROLL_CLIENT_SECRET'],
redirect_uri: 'https://yourapp.com/oauth/callback'
)
For a custom base URL or HTTP timeout:
client = Mobiscroll::Connect::Client.new(
client_id: ENV['MOBISCROLL_CLIENT_ID'],
client_secret: ENV['MOBISCROLL_CLIENT_SECRET'],
redirect_uri: 'https://yourapp.com/oauth/callback',
base_url: 'https://connect.mobiscroll.com/api',
timeout: 60
)
Methods
set_credentials
Stores a token pair the SDK will use on subsequent requests. Typically called after client.auth.get_token or when restoring credentials from persistent storage.
Method: client.set_credentials(tokens)
The token response returned by client.auth.get_token(code).
on_tokens_refreshed
Registers a callback to be invoked whenever the SDK automatically refreshes the access token. Use this to persist the updated tokens so they survive future requests.
Method: client.on_tokens_refreshed { |tokens| ... }
A block that receives the updated TokenResponse after a successful automatic token refresh.
credentials
Returns the currently stored credentials, or nil if none.
Method: client.credentials
Returns: TokenResponse or nil
Token Refresh
The Ruby SDK handles token refresh automatically. When any API call returns a 401 Unauthorized response and the client has a refresh_token stored, the SDK silently exchanges it for a new access token and retries the original request — with no action required from your application.
When the refresh succeeds, the SDK invokes your on_tokens_refreshed callback with the updated TokenResponse. You must register this callback and persist the new tokens, otherwise they will be lost between process restarts.
client.on_tokens_refreshed do |tokens|
# Persist tokens in your database or session store
# so the new access_token and refresh_token survive future requests
session[:access_token] = tokens.access_token
session[:refresh_token] = tokens.refresh_token if tokens.refresh_token
end
If the refresh token itself is invalid or has been revoked, the SDK raises Mobiscroll::Connect::AuthenticationError and the user must re-authorize.
Running more than one instance
Concurrent calls on the same client that hit an expired token deduplicate into a single refresh via a Monitor and condition variable. That covers one client object in one process. It does not reach across processes: separate containers, cluster workers or serverless invocations each refresh on their own, from the tokens they hold in memory.
Connect accepts several processes refreshing the same token at once, but a process that refreshes with a copy that is two or more refreshes out of date revokes the user's authorization — see Refreshing from several instances. So:
- persist the tokens from your
on_tokens_refreshedcallback to storage that every instance reads, and - when a process starts a job or handles a request, call
set_credentialswith the current tokens from that storage instead of relying on the copy it loaded at startup.
Error Handling
All SDK errors are subclasses of Mobiscroll::Connect::Error. Rescue the specific subclass to handle each case:
| Error class | HTTP Status | Extra attribute |
|---|---|---|
AuthenticationError | 401, 403 | — (raised after refresh + retry has been exhausted) |
CalendarPermissionError | 403 | accounts — the accounts that must reconnect |
ValidationError | 400, 422 | details |
NotFoundError | 404 | — |
RateLimitError | 429 | retry_after (seconds) |
ServerError | 5xx | status_code |
NetworkError | — | cause (wraps the underlying Faraday error) |
WebhookVerificationError | — | reason — why the delivery was rejected |
WebhookVerificationError has the code WEBHOOK_VERIFICATION_ERROR. It is raised by webhooks.verify_webhook and Mobiscroll::Connect.verify_webhook_signature, not by API calls; its reason says why the delivery was rejected.
begin
client.events.list(
start: '2025-10-01T00:00:00Z',
end: '2025-10-31T23:59:59Z'
)
rescue Mobiscroll::Connect::AuthenticationError
# Token expired and refresh failed — re-authorize the user
rescue Mobiscroll::Connect::ValidationError => e
puts "Validation failed: #{e.details}"
rescue Mobiscroll::Connect::RateLimitError => e
sleep(e.retry_after)
rescue Mobiscroll::Connect::Error => e
# Catch-all for any other SDK error
puts "SDK error: #{e.message} (#{e.code})"
end
Auth API
The client.auth resource handles the OAuth authorization flow, including generating authorization URLs, exchanging codes for tokens, managing connection status, and disconnecting providers.
Each account returned by get_connection_status reports granted_scopes and calendar_permission_granted. A false flag means the account connected but withheld calendar access on the provider's consent screen, so it can list no calendars until the user reconnects — see Partial consent. Each account also reports syncState: reauth_required means the provider has since rejected the stored credentials and the account has stopped syncing — a different question from calendarPermissionGranted, which only records what was agreed at connect time. See Connection health.
When no connected account has calendar access, calendar and event calls raise CalendarPermissionError — accounts names the accounts that must reconnect. It subclasses AuthenticationError, so existing handlers keep working.
To localize the Connect pages, pass an optional lng to generate_auth_url, e.g. generate_auth_url(user_id: ..., lng: 'es'). When omitted, the UI falls back to the browser's Accept-Language header, then English; Arabic, Hebrew and Persian render right-to-left. See Supported languages for the languages Connect supports.
🔐 OAuth API Reference
auth.generate_auth_url
Builds the URL that starts the authorization flow. Redirect the user to it: once they connect their calendars, Connect redirects them back to your redirect URI with an authorization code. The URL is built locally, without an API request. See GET /authorize.
Method: client.auth.generate_auth_url(user_id:, scope: nil, state: nil, lng: nil)
Returns: String
Your identifier for the user. scope, state and lng are optional keyword arguments.
Usage:
# Generate the authorization URL
auth_url = client.auth.generate_auth_url(
user_id: 'user-456',
# Optional parameters:
# scope: 'read-write',
# state: 'xyz789',
# lng: 'es' # localize the Connect pages
)
# Redirect the user to auth_url
# redirect auth_url
auth.get_token
Exchanges the authorization code from your redirect URI for an access token and a refresh token. The client stores the returned tokens and uses them for the calls that follow. See POST /token.
Method: client.auth.get_token(code)
Returns: Mobiscroll::Connect::TokenResponse
The authorization code received on your redirect URI.
Usage:
# Exchange authorization code for access token
# The client is automatically authenticated with the new token
tokens = client.auth.get_token(code)
# Persist tokens server-side keyed by your user
session[:access_token] = tokens.access_token
session[:refresh_token] = tokens.refresh_token
Calendars API
The client.calendars resource allows you to list available calendars from all connected providers (Google, Outlook, etc.). It corresponds to the /calendars endpoints.
📅 Calendars API Reference
calendars.list
Lists the calendars of every account the user has connected. See GET /calendars.
Method: client.calendars.list
Returns: Array<Mobiscroll::Connect::Calendar>
Usage:
calendars = client.calendars.list
Events API
The client.events resource provides methods to create, read, update, and delete calendar events across all connected accounts. It corresponds to the /events endpoints.
📅 Events API Reference
events.list
Lists events from the user's calendars, one page at a time. When the response contains a next_page_token, pass it to the next call to get the following page. See GET /events.
Method: client.events.list(start: nil, end: nil, calendar_ids: nil, page_size: nil, next_page_token: nil, single_events: nil)
Returns: Mobiscroll::Connect::EventsListResponse
Calendar IDs grouped by provider, for example { Mobiscroll::Connect::Provider::GOOGLE => ['work@company.com'] }. All keyword arguments are optional.
Usage:
# Fetch initial events with date range filter
response = client.events.list(
page_size: 50,
start: '2025-10-01T00:00:00Z',
end: '2025-10-31T23:59:59Z'
)
# Load more events using next_page_token
next_response = client.events.list(
page_size: 50,
next_page_token: response.next_page_token
)
# Filter by specific calendars
filtered = client.events.list(
page_size: 25,
calendar_ids: {
Mobiscroll::Connect::Provider::GOOGLE => ['personal@gmail.com', 'work@company.com']
}
)
# Get recurring event series masters (not expanded)
masters = client.events.list(
page_size: 50,
single_events: false
)
events.create
Creates an event in the given calendar. Pass a Mobiscroll::Connect::RecurrenceRule as recurrence to create a recurring series. See POST /event.
Method: client.events.create(provider:, calendar_id:, title:, start:, end:, **opts)
Returns: Mobiscroll::Connect::CalendarEvent
Optional event fields as keyword arguments, such as description, location, all_day and recurrence. Unrecognized keywords are ignored without an error, so check the spelling.
Usage:
# Create a simple event
event = client.events.create(
provider: Mobiscroll::Connect::Provider::GOOGLE,
calendar_id: 'primary',
title: 'Team Meeting',
description: 'Discuss project updates',
start: '2025-11-01T10:00:00Z',
end: '2025-11-01T11:00:00Z',
location: 'Conference Room A'
)
# Create a recurring event
recurring_event = client.events.create(
provider: Mobiscroll::Connect::Provider::MICROSOFT,
calendar_id: 'AAMkAGVmMDEz...',
title: 'Weekly Standup',
start: '2025-11-01T09:00:00Z',
end: '2025-11-01T09:30:00Z',
all_day: false,
recurrence: Mobiscroll::Connect::RecurrenceRule.new(
frequency: 'WEEKLY',
interval: 1,
count: 10,
by_day: %w[MO WE FR]
)
)
# Create an all-day event
all_day_event = client.events.create(
provider: Mobiscroll::Connect::Provider::APPLE,
calendar_id: 'https://caldav.icloud.com/.../calendars/...',
title: 'Conference',
start: '2025-11-15T00:00:00Z',
end: '2025-11-16T00:00:00Z',
all_day: true,
description: 'Annual tech conference'
)
events.update
Updates an existing event. For an occurrence of a recurring series, set recurring_event_id, and use update_mode to choose which occurrences change: this, following or all. See PUT /event.
Method: client.events.update(provider:, calendar_id:, event_id:, **opts)
Returns: Mobiscroll::Connect::CalendarEvent
The fields you want to change as keyword arguments, plus recurring_event_id and update_mode for recurring events. Unrecognized keywords are ignored without an error.
Usage:
# Update a simple event
updated_event = client.events.update(
provider: Mobiscroll::Connect::Provider::GOOGLE,
calendar_id: 'primary',
event_id: 'event123abc',
title: 'Updated Team Meeting',
start: '2025-11-01T14:00:00Z',
end: '2025-11-01T15:00:00Z'
)
# Update a single instance of a recurring event
updated_instance = client.events.update(
provider: Mobiscroll::Connect::Provider::MICROSOFT,
event_id: 'instance456',
recurring_event_id: 'series123',
calendar_id: 'AAMkAGVmMDEz...',
update_mode: 'this',
title: 'Standup - Special Topic Today',
start: '2025-11-01T09:00:00Z',
end: '2025-11-01T10:00:00Z'
)
# Update all future occurrences
updated_future = client.events.update(
provider: Mobiscroll::Connect::Provider::APPLE,
event_id: 'recurring-event-id',
calendar_id: 'https://caldav.icloud.com/.../calendars/...',
update_mode: 'following',
location: 'New Conference Room B'
)
events.delete
Deletes an event. For an occurrence of a recurring series, set recurring_event_id, and use delete_mode to choose which occurrences are deleted: this, following or all. See DELETE /event.
Method: client.events.delete(provider:, calendar_id:, event_id:, recurring_event_id: nil, delete_mode: nil)
Returns: nil
Parameters: See Request Body for the full field list.
Usage:
# Delete a simple event
client.events.delete(
provider: Mobiscroll::Connect::Provider::GOOGLE,
calendar_id: 'primary',
event_id: 'event123abc'
)
# Delete a single instance of a recurring event
client.events.delete(
provider: Mobiscroll::Connect::Provider::MICROSOFT,
calendar_id: 'AAMkAGVmMDEz...',
event_id: 'instance456',
recurring_event_id: 'series123',
delete_mode: 'this'
)
# Delete entire recurring series
client.events.delete(
provider: Mobiscroll::Connect::Provider::APPLE,
calendar_id: 'https://caldav.icloud.com/.../calendars/...',
event_id: 'recurring-event-id',
delete_mode: 'all'
)
Webhooks API
The client.webhooks resource lets you subscribe a calendar to change notifications, unsubscribe an existing channel, and verify the deliveries Connect sends to your webhook URL. Subscribing and unsubscribing correspond to the /subscribe-webhook and /unsubscribe-webhook endpoints. Connect subscribes and renews channels automatically when a calendar is connected — those two methods are for managing a specific channel explicitly.
🔔 Webhooks API Reference
webhooks.subscribe_webhook
Subscribes a calendar to change notifications. channel_id is optional and generated by the server when omitted. expiration is an optional Unix timestamp in milliseconds. See POST /subscribe-webhook.
Method: client.webhooks.subscribe_webhook(provider:, calendar_id:, channel_id: nil, expiration: nil)
Returns: Mobiscroll::Connect::SubscribeWebhookResponse
Parameters: See Request Parameters for the full field list.
Usage:
subscription = client.webhooks.subscribe_webhook(
provider: Mobiscroll::Connect::Provider::GOOGLE,
calendar_id: 'work@company.com',
channel_id: 'my-channel-123'
)
webhooks.unsubscribe_webhook
Removes an existing webhook subscription. resource_id is the provider resource ID returned by subscribe_webhook, where the provider uses one. See POST /unsubscribe-webhook.
Method: client.webhooks.unsubscribe_webhook(provider:, channel_id:, resource_id: nil)
Returns: Mobiscroll::Connect::UnsubscribeWebhookResponse
Parameters: See Request Parameters for the full field list.
Usage:
client.webhooks.unsubscribe_webhook(
provider: Mobiscroll::Connect::Provider::GOOGLE,
channel_id: 'my-channel-123',
resource_id: 'resource-abc'
)
webhooks.verify_webhook
Checks that a delivery to your webhook URL came from Mobiscroll Connect and was not changed on the way, then returns it parsed. It verifies the Ed25519 signature and rejects a webhook-timestamp more than 5 minutes from your server's clock — see Verifying deliveries. Available from version 1.6.0.
Method: client.webhooks.verify_webhook(payload, headers)
Returns: Mobiscroll::Connect::WebhookDelivery
Raises: Mobiscroll::Connect::WebhookVerificationError when the delivery is not genuine. Respond with a 4xx and do not process it.
The raw request body, exactly as received. Do not pass a parsed Hash: a JSON body parser that runs first changes the bytes, and every signature check fails.
The request headers, as a Hash with any key casing, a Rack env, or Rails' request.headers. Rack-style keys such as HTTP_WEBHOOK_ID are matched too. Must include webhook-id, webhook-timestamp and webhook-signature.
The SDK fetches the public keys from /.well-known/webhook-keys on the first delivery, not when the client is created, and caches them for the whole process, shared by every client instance. It refreshes them as the endpoint's Cache-Control header allows. When no signature matches, it fetches the keys again before rejecting the delivery, at most once a minute, so a key rotation needs no action from you. If a refresh fails, it keeps using the keys it already has. The pinned webhook_public_key is used only when no keys could be fetched at all.
error.reason is a String, one of:
reason | Meaning |
|---|---|
'missing_headers' | webhook-id, webhook-timestamp or webhook-signature is missing |
'invalid_timestamp' | webhook-timestamp is not a Unix time in seconds |
'timestamp_out_of_tolerance' | webhook-timestamp is more than 5 minutes from your server's clock |
'no_public_keys' | No keys could be fetched and no webhook_public_key is configured |
'no_matching_signature' | No signature matches any key — a forged or modified delivery |
'invalid_payload' | The body is not a raw String, or is not a JSON object |
Usage:
require 'sinatra'
require 'mobiscroll-connect'
post '/webhooks/mobiscroll' do
begin
delivery = client.webhooks.verify_webhook(request.body.read, request.env)
rescue Mobiscroll::Connect::WebhookVerificationError => e
# 503 lets Connect retry when the keys could not be loaded; 401 is final.
halt(e.reason == 'no_public_keys' ? 503 : 401)
end
handle_delivery(delivery)
status 204
end
In Rails, pass the raw body and the request headers: client.webhooks.verify_webhook(request.raw_post, request.headers).
Mobiscroll::Connect.verify_webhook_signature
Verifies a delivery against public keys you supply, with no network request. Use it in handlers that cannot make outbound requests; otherwise prefer webhooks.verify_webhook, which keeps the keys current for you. Available from version 1.6.0.
Method: Mobiscroll::Connect.verify_webhook_signature(payload, headers, public_keys, tolerance_seconds: 300, now: nil)
Returns: nil
Raises: Mobiscroll::Connect::WebhookVerificationError, with the same reason values as webhooks.verify_webhook.
The raw request body, exactly as received.
The request headers, as a Hash with any key casing, a Rack env, or Rails' request.headers.
whpk_ public keys. The delivery is genuine if any signature verifies against any of them.
The allowed webhook-timestamp age in either direction, in seconds. Defaults to 300.
Overrides the current Unix time in seconds. Defaults to the system clock.
Usage:
raw_body = request.body.read
Mobiscroll::Connect.verify_webhook_signature(
raw_body, request.env, [ENV.fetch('MOBISCROLL_WEBHOOK_PUBLIC_KEY')]
)
delivery = Mobiscroll::Connect::WebhookDelivery.from_h(JSON.parse(raw_body))
A pinned key stops verifying when Mobiscroll retires it, so you must replace it on every key rotation.