The real problem
Why this is harder than it looks
Calendly's REST API is clean and the documentation is thorough. You can build a working prototype against your own account in an afternoon. The problems arrive when you take this to production for real users.
Calendly uses OAuth 2.0, but you must register your own OAuth app in the Calendly Developer Portal — there is no shared public client you can rely on. That app needs a redirect URI pointing to your infrastructure, and the scopes you request must be explicitly enabled in the app settings before users can authorize. The default scope covers most scheduling operations, but enterprise features like activity logs require the activity_log:read scope, which is only available on Calendly Enterprise plans. Requesting a scope your Calendly app hasn't enabled causes silent authorization failures that are hard to diagnose. In Workspace or organizational deployments, admins can block third-party app access entirely — and your OAuth flow will fail with no actionable error for the user.
Beyond initial authorization, you need per-user token isolation across a multi-tenant product. Calendly's API surfaces are user-scoped: event types, scheduled events, availability schedules, and routing forms all resolve relative to the authenticated user's organization context. A token from one user cannot be reused for another — and if your routing is wrong, calls succeed silently against the wrong user's data. Tokens refresh on a standard OAuth cycle, but revocation (manual by the user, or triggered by org admin policy) moves the connection to an error state with no automatic recovery.
Scalekit handles OAuth app registration plumbing, token refresh, revocation detection, and per-user isolation. Your agent names a tool and passes parameters. The auth lifecycle is not your problem.
Capabilities
What your agent can do with Calendly
Once connected, your agent has 53 pre-built tools covering the full Calendly API:
- Query and manage scheduled events: list upcoming and past events with time/status filters, retrieve event details, cancel events with optional reason
- Manage event types and availability: create, update, and list event types; read and update availability schedules and rules per event type
- Handle invitees end-to-end: list invitees for events, mark no-shows, create invitees, and manage routing form submissions
- Manage organizations and memberships: send and revoke org invitations, list and remove memberships, retrieve group and group relationship data
- Create scheduling links and shares: generate single-use or limited-use scheduling links; create shareable scheduling pages with custom date ranges and availability overrides
- Subscribe to webhooks: create, list, and delete webhook subscriptions for real-time event notifications
Setup context
What we're building
This guide connects a scheduling assistant agent to Calendly — helping users query their upcoming meetings, manage event types, and track invitee activity without leaving your product.
🤖
Example agent
Scheduling assistant querying upcoming events, managing event types, and tracking invitee activity on behalf of each user
🔐
Auth model
B2B SaaS — each user connects their own Calendly account. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Calendly secrets, no user tokens, nothing belonging to your users.
pip install scalekit-sdk-python
npm install @scalekit-sdk/node
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const actions = scalekit.actions;
Already have credentials?
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account — a dedicated auth context scoped to their Calendly organization. The identifier is any unique string from your system — a UUID, email, or whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="calendly",
identifier="user_cal_789" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
const response = await actions.getOrCreateConnectedAccount({
connectionName: "calendly",
identifier: "user_cal_789" // your internal user ID
});
const connectedAccount = response.connectedAccount;
console.log(`Status: ${connectedAccount.status}`);
// Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="calendly",
identifier="user_cal_789"
)
# Redirect user → Calendly's native OAuth consent screen
# Scalekit captures the token on callback
return redirect(link.link)
if (connectedAccount.status !== "ACTIVE") {
const { link } = await actions.getAuthorizationLink({
connectionName: "calendly",
identifier: "user_cal_789"
});
// Redirect user → Calendly's native OAuth consent screen
// Scalekit captures the token on callback
return redirect(link);
}
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access tokens refresh before expiry. If a user's token is revoked — by account password change, admin policy, or manual revocation — the account moves to REVOKED. No silent failures. Check account.status before critical operations.
Bring Your Own Credentials — required for production
Calendly requires you to register your own OAuth app at developer.calendly.com and supply your Client ID and Secret. In the Scalekit dashboard, go to Agent Auth → Connections → your Calendly connection and add your credentials. Token management stays fully handled.
Calling Calendly
4 Calling Calendly: What your agent writes
With the connected account active, your agent calls Calendly actions using actions.execute_tool(). Name the tool, pass parameters. Scalekit handles token retrieval and request construction.
Get the current user's profile
Retrieve the authenticated user's Calendly profile — including their organization URI, which you'll need for org-scoped queries like listing memberships or scheduled events.
result = actions.execute_tool(
identifier="user_cal_789",
tool_name="calendly_current_user_get",
tool_input={}
)
# Returns: user URI, name, email, organization URI, timezone, avatar URL
const result = await actions.executeTool({
identifier: "user_cal_789",
toolName: "calendly_current_user_get",
toolInput: {}
});
// Returns: user URI, name, email, organization URI, timezone, avatar URL
List upcoming scheduled events
Fetch all active events for a user within a time range. Use min_start_time and max_start_time to scope the window. Filter by status to separate active from canceled events.
result = actions.execute_tool(
identifier="user_cal_789",
tool_name="calendly_scheduled_events_list",
tool_input={
"user": "https://api.calendly.com/users/AAAA1234",
"min_start_time": "2026-05-01T00:00:00Z",
"max_start_time": "2026-05-31T23:59:59Z",
"status": "active",
"sort": "start_time:asc",
"count": 50
}
)
# Returns: list of scheduled events with start/end time, event type, invitees URI
const result = await actions.executeTool({
identifier: "user_cal_789",
toolName: "calendly_scheduled_events_list",
toolInput: {
"user": "https://api.calendly.com/users/AAAA1234",
"min_start_time": "2026-05-01T00:00:00Z",
"max_start_time": "2026-05-31T23:59:59Z",
"status": "active",
"sort": "start_time:asc",
"count": 50
}
});
// Returns: list of scheduled events with start/end time, event type, invitees URI
List event types for a user
Retrieve all event types available for a user — useful for presenting scheduling options or updating availability rules. Pass active: true to exclude archived types.
result = actions.execute_tool(
identifier="user_cal_789",
tool_name="calendly_event_types_list",
tool_input={
"user": "https://api.calendly.com/users/AAAA1234",
"active": True,
"count": 20
}
)
# Returns: event type URIs, names, durations, scheduling URLs, and active status
const result = await actions.executeTool({
identifier: "user_cal_789",
toolName: "calendly_event_types_list",
toolInput: {
"user": "https://api.calendly.com/users/AAAA1234",
"active": true,
"count": 20
}
});
// Returns: event type URIs, names, durations, scheduling URLs, and active status
Create a single-use scheduling link
Generate a one-time scheduling link for a specific event type — useful for sending personalized booking links without exposing your main scheduling page. Set max_event_count to 1 for single-use.
result = actions.execute_tool(
identifier="user_cal_789",
tool_name="calendly_scheduling_link_create",
tool_input={
"owner": "https://api.calendly.com/event_types/BBBB5678",
"owner_type": "EventType",
"max_event_count": 1
}
)
# Returns: booking_url — a single-use scheduling link ready to share
const result = await actions.executeTool({
identifier: "user_cal_789",
toolName: "calendly_scheduling_link_create",
toolInput: {
"owner": "https://api.calendly.com/event_types/BBBB5678",
"owner_type": "EventType",
"max_event_count": 1
}
});
// Returns: booking_url — a single-use scheduling link ready to share
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to call; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
cal_tools = get_tools(
connection_name="calendly",
identifier="user_cal_789"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a scheduling assistant. Use the available tools to help manage Calendly events, event types, and invitee activity."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), cal_tools, prompt)
result = AgentExecutor(agent=agent, tools=cal_tools).invoke({
"input": "Show me all my meetings next week and create a one-time booking link for my 30-minute intro call"
})
import { ChatAnthropic } from "@langchain/anthropic";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { getTools } from "@scalekit-sdk/langchain";
const calTools = getTools({
connectionName: "calendly",
identifier: "user_cal_789"
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a scheduling assistant. Use the available tools to help manage Calendly events, event types, and invitee activity."],
new MessagesPlaceholder("chat_history", true),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createToolCallingAgent({
llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
tools: calTools,
prompt
});
const result = await AgentExecutor.fromAgentAndTools({
agent,
tools: calTools
}).invoke({
input: "Show me all my meetings next week and create a one-time booking link for my 30-minute intro call"
});
Other frameworks supported
Tool reference
All 53 Calendly tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
calendly_current_user_get
Returns the profile of the currently authenticated Calendly user, including organization URI and timezone
Returns the profile of a specific Calendly user by UUID
calendly_user_busy_times_list
Returns busy time blocks for a user within a specified ISO 8601 time range
calendly_user_availability_schedules_list
Lists all availability schedules for a specified user URI
calendly_user_availability_schedule_get
Returns a single availability schedule for a user by UUID
calendly_scheduled_events_list
Lists scheduled events for a user or org with time range, status, and sort filters. Supports pagination
calendly_scheduled_event_get
Returns details of a specific scheduled event by UUID, including start/end time and event type
calendly_scheduled_event_cancel
Cancels a scheduled event by UUID with an optional cancellation reason
calendly_event_types_list
Lists event types for a user or organization. Filter by active status. Supports pagination
Returns details of a specific event type by UUID including duration, slug, and scheduling URL
calendly_event_type_create
Creates a new event type with duration, host, name, and optional color and description
calendly_event_type_update
Updates an existing event type — name, description, duration, or color. Only supplied fields are changed
calendly_event_type_memberships_list
Lists hosts (memberships) associated with a specific event type URI
calendly_event_type_available_times_list
Returns available booking slots for an event type within a given ISO 8601 date range
calendly_event_type_availability_schedules_list
Lists availability schedules (rules) for a specific event type URI
calendly_event_type_availability_schedules_update
Updates the availability rules for an event type. Accepts rules array with intervals and weekday fields
calendly_one_off_event_type_create
Creates a one-off event type with a specific date window, host, duration, and optional co-hosts and location
calendly_event_invitees_list
Lists invitees for a scheduled event. Filter by email or status (active/canceled). Supports pagination
calendly_event_invitee_get
Returns details of a specific invitee by event UUID and invitee UUID
Creates a new invitee for a scheduled event with name, email, and optional timezone
calendly_invitee_no_show_create
Marks an invitee as a no-show using their full invitee URI
calendly_invitee_no_show_get
Returns a specific no-show record by UUID
calendly_invitee_no_show_delete
Removes the no-show mark from an invitee no-show record by UUID
calendly_scheduling_link_create
Creates a single-use or limited-use booking link for a specific event type. Set max_event_count to 1 for single-use
Creates a shareable scheduling page with optional duration override, date range, availability rules, and booking limits
calendly_organization_get
Returns details of a specific Calendly organization by UUID
calendly_organization_memberships_list
Lists organization memberships filtered by org URI or user URI. Supports email filter and pagination
calendly_organization_membership_get
Returns details of a specific membership record by UUID
calendly_organization_membership_delete
Removes a user from an organization by deleting their membership by UUID
calendly_organization_invitation_create
Sends an invitation to join a Calendly organization by org UUID and invitee email
calendly_organization_invitation_get
Returns details of a specific org invitation by org UUID and invitation UUID
calendly_organization_invitations_list
Lists pending invitations for an organization. Filter by email or status (pending/accepted/declined)
calendly_organization_invitation_revoke
Revokes a pending organization invitation by invitation UUID and org UUID
Lists groups in a Calendly organization with sorting and pagination support
Returns a single group record by UUID
calendly_group_relationships_list
Lists group relationships in a Calendly organization by org URI
calendly_group_relationship_get
Returns a single group relationship record by UUID
calendly_routing_forms_list
Lists routing forms for a Calendly organization by org URI
calendly_routing_form_get
Returns details of a specific routing form by UUID
calendly_routing_form_submissions_list
Lists submissions for a specific routing form URI with pagination
calendly_routing_form_submission_get
Returns details of a specific routing form submission by UUID
calendly_routing_form_submission_get_by_uuid
Returns a single routing form submission by UUID (alternative endpoint)
calendly_webhook_subscription_create
Creates a webhook subscription for specified event types at a callback URL. Supports user or org scope
calendly_webhook_subscriptions_list
Lists webhook subscriptions filtered by org URI, user URI, or scope. Supports pagination
calendly_webhook_subscription_get
Returns details of a specific webhook subscription by UUID
calendly_webhook_subscription_delete
Deletes a webhook subscription by UUID, stopping future event notifications
Lists available meeting locations for a user or organization
calendly_outgoing_communications_list
Lists outgoing emails and notifications for an organization with sort and pagination support
calendly_sample_webhook_data_get
Returns a sample webhook payload for a specified event type — useful for testing webhook integrations
calendly_data_compliance_events_delete
Deletes all event data within a specified time range for compliance. Destructive and irreversible
calendly_data_compliance_invitees_delete
Deletes all invitee data for specified email addresses for compliance purposes. Destructive and irreversible
calendly_activity_log_list
Lists audit log entries for a Calendly organization. Filter by action type, actor, and time range. Requires Enterprise plan and activity_log:read scope
Connector notes
Calendly-specific behavior
All resource references use full URIs, not bare UUIDs
Calendly's API uses full resource URIs (e.g. https://api.calendly.com/users/xxx) as identifiers in most filter and reference fields — not bare UUIDs. Passing a UUID where a URI is expected returns a 400 or silent mismatch. Use calendly_current_user_get to retrieve the authenticated user's full URI, then use that as the base for subsequent calls.
Scopes must be enabled in your Calendly OAuth app before requesting them
The scopes you configure in Scalekit must match what is enabled in your Calendly Developer Portal app settings. Requesting a scope not enabled in your app causes an invalid_scope error at authorization. The activity_log:read scope is only available on Calendly Enterprise plans — attempting it on lower-tier accounts fails silently or returns empty results.
Data compliance tools are destructive and irreversible
calendly_data_compliance_events_delete and calendly_data_compliance_invitees_delete permanently remove data with no undo path. Add confirmation gates in your agent logic before invoking these tools in any automated or semi-automated workflow.
Infrastructure decision
Why not build this yourself
The Calendly OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
You must register and maintain your own Calendly OAuth app — including redirect URI registration, scope management, and re-verification if you add new capabilities later
PROBLEM 02
Per-user token isolation across a multi-tenant product — one user's Calendly credentials must never resolve to another user's organization context
PROBLEM 03
Token revocation by admin policy or manual user action requires per-user detection and a re-authorization flow — without it, calls fail silently with no recovery path
PROBLEM 04
Scope mismatches and plan-gated scopes (like activity_log:read on Enterprise) produce silent failures in production that are difficult to diagnose without deep Calendly-specific knowledge
That's one connector. Your agent product will eventually need Salesforce, Gmail, Slack, HubSpot, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.