NYC

calendly-api

SKILL.md

Calendly API Skill

Master Calendly scheduling automation using the REST API v2. This skill covers event type management, availability configuration, booking workflows, webhook integrations, and automated scheduling patterns for building seamless meeting coordination.

When to Use This Skill

USE when:

  • Automating interview scheduling workflows
  • Building meeting booking integrations
  • Creating round-robin scheduling systems
  • Tracking scheduled events programmatically
  • Integrating calendars with CRM systems
  • Building appointment reminders
  • Creating custom booking confirmation flows
  • Automating follow-up sequences after meetings
  • Syncing Calendly with external calendars
  • Building scheduling analytics dashboards

DON'T USE when:

  • Simple calendar display (use Google Calendar API)
  • Real-time video calls (use Zoom/Teams API)
  • Complex resource scheduling (use specialized tools)
  • Internal meeting coordination only (use calendar apps)
  • One-off manual scheduling (use Calendly UI directly)

Prerequisites

Calendly API Setup

# 1. Get API credentials at https://calendly.com/integrations/api_webhooks
# 2. Create a Personal Access Token or OAuth app
# 3. Note: API v2 requires organization-level access for some endpoints

# Personal Access Token:
# - Go to Integrations > API & Webhooks
# - Generate a new token
# - Copy the token (shown only once)

# OAuth 2.0 App:
# - Go to https://developer.calendly.com/
# - Create an OAuth application
# - Configure redirect URIs
# - Note client_id and client_secret

# Required OAuth Scopes:
# - default                    - Basic access
# - organization:read          - Read organization data
# - organization:write         - Manage organization
# - user:read                  - Read user profiles
# - scheduling_link:read       - Read scheduling links
# - event_type:read            - Read event types
# - event_type:write           - Manage event types
# - scheduled_event:read       - Read scheduled events
# - scheduled_event:write      - Manage scheduled events
# - invitee:read               - Read invitee data
# - webhook:read               - Read webhooks
# - webhook:write              - Manage webhooks

Python Environment Setup

# Create virtual environment
python -m venv calendly-env
source calendly-env/bin/activate  # Linux/macOS
# calendly-env\Scripts\activate   # Windows

# Install dependencies
pip install requests python-dotenv httpx aiohttp

# Create requirements.txt
cat > requirements.txt << 'EOF'
requests>=2.31.0
python-dotenv>=1.0.0
httpx>=0.25.0
aiohttp>=3.9.0
pydantic>=2.5.0
EOF

# Environment variables
cat > .env << 'EOF'
CALENDLY_API_KEY=your-personal-access-token
CALENDLY_CLIENT_ID=your-oauth-client-id
CALENDLY_CLIENT_SECRET=your-oauth-client-secret
CALENDLY_WEBHOOK_SECRET=your-webhook-signing-secret
EOF

API Client Setup

# client.py
# ABOUTME: Calendly API client with authentication
# ABOUTME: Handles requests, pagination, and error handling

import os
import requests
from typing import Optional, Dict, Any, List
from dotenv import load_dotenv

load_dotenv()


class CalendlyClient:
    """Calendly API v2 client"""

    BASE_URL = "https://api.calendly.com"

    def __init__(self, api_key: str = None):
        self.api_key = api_key or os.environ.get("CALENDLY_API_KEY")
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        })
        self._current_user = None
        self._organization = None

    def _request(
        self,
        method: str,
        endpoint: str,
        params: Dict = None,
        json: Dict = None,
    ) -> Dict:
        """Make an API request"""
        url = f"{self.BASE_URL}{endpoint}"

        response = self.session.request(
            method=method,
            url=url,
            params=params,
            json=json,
        )

        if response.status_code == 429:
            # Rate limited
            retry_after = int(response.headers.get("Retry-After", 60))
            raise Exception(f"Rate limited. Retry after {retry_after}s")

        response.raise_for_status()

        if response.status_code == 204:
            return {}

        return response.json()

    def get(self, endpoint: str, params: Dict = None) -> Dict:
        """GET request"""
        return self._request("GET", endpoint, params=params)

    def post(self, endpoint: str, json: Dict = None) -> Dict:
        """POST request"""
        return self._request("POST", endpoint, json=json)

    def delete(self, endpoint: str) -> Dict:
        """DELETE request"""
        return self._request("DELETE", endpoint)

    def paginate(
        self,
        endpoint: str,
        params: Dict = None,
        key: str = "collection",
        limit: int = None,
    ) -> List[Dict]:
        """Paginate through results"""
        params = params or {}
        params["count"] = 100
        results = []

        while True:
            response = self.get(endpoint, params)
            items = response.get(key, [])
            results.extend(items)

            if limit and len(results) >= limit:
                return results[:limit]

            pagination = response.get("pagination", {})
            next_page_token = pagination.get("next_page_token")

            if not next_page_token:
                break

            params["page_token"] = next_page_token

        return results

    @property
    def current_user(self) -> Dict:
        """Get current user info (cached)"""
        if not self._current_user:
            response = self.get("/users/me")
            self._current_user = response.get("resource")
        return self._current_user

    @property
    def user_uri(self) -> str:
        """Get current user URI"""
        return self.current_user["uri"]

    @property
    def organization_uri(self) -> str:
        """Get current organization URI"""
        return self.current_user["current_organization"]


# Global client instance
client = CalendlyClient()

Core Capabilities

1. User and Organization Management

# users.py
# ABOUTME: User and organization management
# ABOUTME: Retrieve user profiles, organization info, memberships

from client import client


def get_current_user() -> dict:
    """Get the current authenticated user"""
    response = client.get("/users/me")
    user = response.get("resource", {})

    return {
        "uri": user.get("uri"),
        "name": user.get("name"),
        "email": user.get("email"),
        "slug": user.get("slug"),
        "scheduling_url": user.get("scheduling_url"),
        "timezone": user.get("timezone"),
        "organization": user.get("current_organization"),
    }


def get_user_by_uri(user_uri: str) -> dict:
    """Get a user by their URI"""
    # Extract UUID from URI
    uuid = user_uri.split("/")[-1]
    response = client.get(f"/users/{uuid}")
    return response.get("resource", {})


def get_organization(organization_uri: str = None) -> dict:
    """Get organization details"""
    org_uri = organization_uri or client.organization_uri
    uuid = org_uri.split("/")[-1]
    response = client.get(f"/organizations/{uuid}")
    return response.get("resource", {})


def list_organization_memberships(
    organization_uri: str = None,
    email: str = None,
) -> list:
    """List organization memberships"""
    org_uri = organization_uri or client.organization_uri

    params = {"organization": org_uri}
    if email:
        params["email"] = email

    return client.paginate("/organization_memberships", params=params)


def get_user_availability_schedules(user_uri: str = None) -> list:
    """Get user's availability schedules"""
    user = user_uri or client.user_uri
    params = {"user": user}
    return client.paginate("/user_availability_schedules", params=params)


def get_user_busy_times(
    user_uri: str = None,
    start_time: str = None,
    end_time: str = None,
) -> list:
    """Get user's busy times for a date range

    Times should be ISO 8601 format: 2026-01-17T00:00:00Z
    """
    user = user_uri or client.user_uri
    params = {
        "user": user,
        "start_time": start_time,
        "end_time": end_time,
    }
    response = client.get("/user_busy_times", params=params)
    return response.get("collection", [])


if __name__ == "__main__":
    # Get current user
    user = get_current_user()
    print(f"User: {user['name']} ({user['email']})")
    print(f"Scheduling URL: {user['scheduling_url']}")

    # List memberships
    memberships = list_organization_memberships()
    print(f"\nOrganization has {len(memberships)} members")

2. Event Types

# event_types.py
# ABOUTME: Event type management
# ABOUTME: Create, list, and configure event types

from client import client
from typing import Optional, List


def list_event_types(
    user_uri: str = None,
    organization_uri: str = None,
    active: bool = True,
) -> list:
    """List all event types for a user or organization"""
    params = {}

    if user_uri:
        params["user"] = user_uri
    elif organization_uri:
        params["organization"] = organization_uri
    else:
        params["user"] = client.user_uri

    if active is not None:
        params["active"] = str(active).lower()

    return client.paginate("/event_types", params=params)


def get_event_type(event_type_uri: str) -> dict:
    """Get event type details"""
    uuid = event_type_uri.split("/")[-1]
    response = client.get(f"/event_types/{uuid}")
    return response.get("resource", {})


def get_event_type_by_slug(slug: str, user_uri: str = None) -> Optional[dict]:
    """Find event type by slug"""
    event_types = list_event_types(user_uri=user_uri)

    for et in event_types:
        if et.get("slug") == slug:
            return et

    return None


def get_available_times(
    event_type_uri: str,
    start_time: str,
    end_time: str,
) -> list:
    """Get available time slots for an event type

    Times should be ISO 8601 format: 2026-01-17T00:00:00Z
    """
    params = {
        "event_type": event_type_uri,
        "start_time": start_time,
        "end_time": end_time,
    }
    response = client.get("/event_type_available_times", params=params)
    return response.get("collection", [])


def format_event_type_summary(event_type: dict) -> dict:
    """Format event type for display"""
    return {
        "name": event_type.get("name"),
        "slug": event_type.get("slug"),
        "duration": event_type.get("duration"),
        "scheduling_url": event_type.get("scheduling_url"),
        "type": event_type.get("type"),  # StandardEventType, AdhocEventType
        "kind": event_type.get("kind"),  # solo, round_robin, collective
        "active": event_type.get("active"),
        "description": event_type.get("description_plain"),
    }


def list_event_types_summary(user_uri: str = None) -> list:
    """Get summarized list of event types"""
    event_types = list_event_types(user_uri=user_uri)
    return [format_event_type_summary(et) for et in event_types]


if __name__ == "__main__":
    # List all event types
    event_types = list_event_types_summary()

    print("Available Event Types:")
    for et in event_types:
        status = "Active" if et["active"] else "Inactive"
        print(f"  - {et['name']} ({et['duration']} min) [{status}]")
        print(f"    URL: {et['scheduling_url']}")

    # Get available times for an event type
    if event_types:
        et_uri = event_types[0].get("uri")
        from datetime import datetime, timedelta

        start = datetime.now().isoformat() + "Z"
        end = (datetime.now() + timedelta(days=7)).isoformat() + "Z"

        times = get_available_times(et_uri, start, end)
        print(f"\nAvailable slots: {len(times)}")

3. Scheduled Events

# scheduled_events.py
# ABOUTME: Scheduled event management
# ABOUTME: List, retrieve, and cancel scheduled events

from client import client
from typing import Optional, List
from datetime import datetime, timedelta


def list_scheduled_events(
    user_uri: str = None,
    organization_uri: str = None,
    min_start_time: str = None,
    max_start_time: str = None,
    status: str = "active",
    invitee_email: str = None,
    sort: str = "start_time:asc",
) -> list:
    """List scheduled events

    status: active, canceled
    sort: start_time:asc, start_time:desc
    """
    params = {
        "status": status,
        "sort": sort,
    }

    if user_uri:
        params["user"] = user_uri
    elif organization_uri:
        params["organization"] = organization_uri
    else:
        params["user"] = client.user_uri

    if min_start_time:
        params["min_start_time"] = min_start_time
    if max_start_time:
        params["max_start_time"] = max_start_time
    if invitee_email:
        params["invitee_email"] = invitee_email

    return client.paginate("/scheduled_events", params=params)


def get_scheduled_event(event_uri: str) -> dict:
    """Get scheduled event details"""
    uuid = event_uri.split("/")[-1]
    response = client.get(f"/scheduled_events/{uuid}")
    return response.get("resource", {})


def cancel_scheduled_event(event_uri: str, reason: str = None) -> dict:
    """Cancel a scheduled event"""
    uuid = event_uri.split("/")[-1]
    data = {}
    if reason:
        data["reason"] = reason

    response = client.post(f"/scheduled_events/{uuid}/cancellation", json=data)
    return response.get("resource", {})


def get_upcoming_events(
    user_uri: str = None,
    days_ahead: int = 7,
) -> list:
    """Get upcoming events for the next N days"""
    now = datetime.utcnow()
    end = now + timedelta(days=days_ahead)

    return list_scheduled_events(
        user_uri=user_uri,
        min_start_time=now.isoformat() + "Z",
        max_start_time=end.isoformat() + "Z",
        status="active",
    )


def get_past_events(
    user_uri: str = None,
    days_back: int = 30,
) -> list:
    """Get past events from the last N days"""
    now = datetime.utcnow()
    start = now - timedelta(days=days_back)

    return list_scheduled_events(
        user_uri=user_uri,
        min_start_time=start.isoformat() + "Z",
        max_start_time=now.isoformat() + "Z",
        status="active",
        sort="start_time:desc",
    )


def format_event_summary(event: dict) -> dict:
    """Format event for display"""
    return {
        "uri": event.get("uri"),
        "name": event.get("name"),
        "start_time": event.get("start_time"),
        "end_time": event.get("end_time"),
        "status": event.get("status"),
        "location": event.get("location", {}).get("type"),
        "event_type": event.get("event_type"),
        "guests_count": len(event.get("event_guests", [])),
        "cancellation": event.get("cancellation"),
    }


def get_events_by_email(email: str, user_uri: str = None) -> list:
    """Find all events with a specific invitee email"""
    return list_scheduled_events(
        user_uri=user_uri,
        invitee_email=email,
    )


if __name__ == "__main__":
    # Get upcoming events
    events = get_upcoming_events(days_ahead=14)

    print(f"Upcoming events: {len(events)}")
    for event in events:
        summary = format_event_summary(event)
        print(f"  - {summary['name']} at {summary['start_time']}")

    # Get events for specific invitee
    email_events = get_events_by_email("john@example.com")
    print(f"\nEvents with john@example.com: {len(email_events)}")

4. Invitees

# invitees.py
# ABOUTME: Invitee management for scheduled events
# ABOUTME: Retrieve invitee details and custom answers

from client import client
from typing import Optional, List


def list_invitees(
    event_uri: str,
    status: str = None,
    email: str = None,
) -> list:
    """List invitees for a scheduled event

    status: active, canceled
    """
    uuid = event_uri.split("/")[-1]
    params = {}

    if status:
        params["status"] = status
    if email:
        params["email"] = email

    return client.paginate(f"/scheduled_events/{uuid}/invitees", params=params)


def get_invitee(invitee_uri: str) -> dict:
    """Get invitee details"""
    # Parse invitee URI to get event and invitee UUIDs
    parts = invitee_uri.split("/")
    event_uuid = parts[-3]
    invitee_uuid = parts[-1]

    response = client.get(f"/scheduled_events/{event_uuid}/invitees/{invitee_uuid}")
    return response.get("resource", {})


def get_invitee_no_show(invitee_uri: str) -> Optional[dict]:
    """Get no-show status for an invitee"""
    parts = invitee_uri.split("/")
    invitee_uuid = parts[-1]

    try:
        response = client.get(f"/invitee_no_shows/{invitee_uuid}")
        return response.get("resource")
    except Exception:
        return None


def mark_invitee_no_show(invitee_uri: str) -> dict:
    """Mark an invitee as a no-show"""
    response = client.post("/invitee_no_shows", json={"invitee": invitee_uri})
    return response.get("resource", {})


def unmark_invitee_no_show(no_show_uri: str) -> bool:
    """Remove no-show status from an invitee"""
    uuid = no_show_uri.split("/")[-1]
    client.delete(f"/invitee_no_shows/{uuid}")
    return True


def format_invitee_summary(invitee: dict) -> dict:
    """Format invitee for display"""
    return {
        "uri": invitee.get("uri"),
        "name": invitee.get("name"),
        "email": invitee.get("email"),
        "status": invitee.get("status"),
        "timezone": invitee.get("timezone"),
        "created_at": invitee.get("created_at"),
        "rescheduled": invitee.get("rescheduled"),
        "questions_and_answers": [
            {
                "question": qa.get("question"),
                "answer": qa.get("answer"),
            }
            for qa in invitee.get("questions_and_answers", [])
        ],
        "tracking": invitee.get("tracking", {}),
        "utm_parameters": {
            "source": invitee.get("utm_source"),
            "medium": invitee.get("utm_medium"),
            "campaign": invitee.get("utm_campaign"),
        },
    }


def get_invitee_custom_answers(invitee: dict) -> dict:
    """Extract custom question answers from invitee"""
    answers = {}
    for qa in invitee.get("questions_and_answers", []):
        question = qa.get("question")
        answer = qa.get("answer")
        answers[question] = answer
    return answers


def get_all_invitees_for_events(event_uris: list) -> list:
    """Get invitees for multiple events"""
    all_invitees = []

    for event_uri in event_uris:
        invitees = list_invitees(event_uri)
        for invitee in invitees:
            invitee["event_uri"] = event_uri
        all_invitees.extend(invitees)

    return all_invitees


if __name__ == "__main__":
    from scheduled_events import get_upcoming_events

    # Get upcoming events and their invitees
    events = get_upcoming_events(days_ahead=7)

    for event in events[:5]:
        print(f"\nEvent: {event['name']}")
        invitees = list_invitees(event["uri"])

        for inv in invitees:
            summary = format_invitee_summary(inv)
            print(f"  - {summary['name']} ({summary['email']})")

            if summary["questions_and_answers"]:
                for qa in summary["questions_and_answers"]:
                    print(f"    Q: {qa['question']}")
                    print(f"    A: {qa['answer']}")

5. Webhooks

# webhooks.py
# ABOUTME: Webhook subscription management
# ABOUTME: Create, manage, and handle webhook events

from client import client
import hmac
import hashlib
from typing import Optional, List


def list_webhook_subscriptions(
    organization_uri: str = None,
    user_uri: str = None,
    scope: str = None,
) -> list:
    """List webhook subscriptions

    scope: organization, user
    """
    params = {}

    if organization_uri:
        params["organization"] = organization_uri
    elif user_uri:
        params["user"] = user_uri
    else:
        params["organization"] = client.organization_uri

    if scope:
        params["scope"] = scope

    return client.paginate("/webhook_subscriptions", params=params)


def create_webhook_subscription(
    url: str,
    events: list,
    organization_uri: str = None,
    user_uri: str = None,
    signing_key: str = None,
) -> dict:
    """Create a webhook subscription

    events: invitee.created, invitee.canceled, routing_form_submission.created
    """
    data = {
        "url": url,
        "events": events,
    }

    if organization_uri:
        data["organization"] = organization_uri
        data["scope"] = "organization"
    elif user_uri:
        data["user"] = user_uri
        data["scope"] = "user"
    else:
        data["organization"] = client.organization_uri
        data["scope"] = "organization"

    if signing_key:
        data["signing_key"] = signing_key

    response = client.post("/webhook_subscriptions", json=data)
    return response.get("resource", {})


def get_webhook_subscription(subscription_uri: str) -> dict:
    """Get webhook subscription details"""
    uuid = subscription_uri.split("/")[-1]
    response = client.get(f"/webhook_subscriptions/{uuid}")
    return response.get("resource", {})


def delete_webhook_subscription(subscription_uri: str) -> bool:
    """Delete a webhook subscription"""
    uuid = subscription_uri.split("/")[-1]
    client.delete(f"/webhook_subscriptions/{uuid}")
    return True


def verify_webhook_signature(
    payload: bytes,
    signature: str,
    signing_key: str,
    tolerance: int = 180,
) -> bool:
    """Verify Calendly webhook signature

    Calendly uses HMAC-SHA256 for webhook signatures
    """
    import time

    # Parse signature header
    # Format: t=timestamp,v1=signature
    parts = dict(p.split("=", 1) for p in signature.split(","))

    timestamp = int(parts.get("t", 0))
    expected_sig = parts.get("v1", "")

    # Check timestamp tolerance
    if abs(time.time() - timestamp) > tolerance:
        return False

    # Compute expected signature
    signed_payload = f"{timestamp}.{payload.decode()}"
    computed_sig = hmac.new(
        signing_key.encode(),
        signed_payload.encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(computed_sig, expected_sig)


# Webhook event types
WEBHOOK_EVENTS = {
    "invitee.created": "When a new invitee schedules an event",
    "invitee.canceled": "When an invitee cancels an event",
    "routing_form_submission.created": "When a routing form is submitted",
}


class WebhookHandler:
    """Handler for Calendly webhook events"""

    def __init__(self, signing_key: str = None):
        self.signing_key = signing_key
        self.handlers = {}

    def on(self, event: str):
        """Decorator to register an event handler"""
        def decorator(func):
            self.handlers[event] = func
            return func
        return decorator

    def handle(self, payload: dict) -> dict:
        """Handle an incoming webhook event"""
        event = payload.get("event")
        data = payload.get("payload", {})

        handler = self.handlers.get(event)
        if handler:
            return handler(data)

        return {"handled": False, "event": event}


# Example webhook handler
webhook = WebhookHandler()


@webhook.on("invitee.created")
def handle_new_booking(data: dict) -> dict:
    """Handle new booking webhook"""
    invitee = data.get("invitee", {})
    event = data.get("scheduled_event", {})

    return {
        "handled": True,
        "action": "booking_created",
        "invitee_email": invitee.get("email"),
        "event_name": event.get("name"),
        "start_time": event.get("start_time"),
    }


@webhook.on("invitee.canceled")
def handle_cancellation(data: dict) -> dict:
    """Handle cancellation webhook"""
    invitee = data.get("invitee", {})
    cancellation = invitee.get("cancellation", {})

    return {
        "handled": True,
        "action": "booking_canceled",
        "invitee_email": invitee.get("email"),
        "reason": cancellation.get("reason"),
        "canceled_by": cancellation.get("canceled_by"),
    }


if __name__ == "__main__":
    # List existing webhooks
    webhooks = list_webhook_subscriptions()
    print(f"Active webhooks: {len(webhooks)}")

    for wh in webhooks:
        print(f"  - {wh['callback_url']}")
        print(f"    Events: {', '.join(wh['events'])}")
        print(f"    Scope: {wh['scope']}")

    # Create a new webhook
    new_webhook = create_webhook_subscription(
        url="https://example.com/webhooks/calendly",
        events=["invitee.created", "invitee.canceled"],
    )
    print(f"\nCreated webhook: {new_webhook['uri']}")

6. Scheduling Links and Routing

# scheduling.py
# ABOUTME: Scheduling links and routing forms
# ABOUTME: Single-use links, routing, and booking customization

from client import client
from typing import Optional


def create_single_use_link(event_type_uri: str, max_event_count: int = 1) -> dict:
    """Create a single-use scheduling link

    These links can only be used for a limited number of bookings
    """
    response = client.post(
        "/scheduling_links",
        json={
            "max_event_count": max_event_count,
            "owner": event_type_uri,
            "owner_type": "EventType",
        },
    )
    return response.get("resource", {})


def get_scheduling_link(link_uri: str) -> dict:
    """Get scheduling link details"""
    uuid = link_uri.split("/")[-1]
    response = client.get(f"/scheduling_links/{uuid}")
    return response.get("resource", {})


def list_routing_forms(organization_uri: str = None) -> list:
    """List routing forms"""
    org = organization_uri or client.organization_uri
    params = {"organization": org}
    return client.paginate("/routing_forms", params=params)


def get_routing_form(form_uri: str) -> dict:
    """Get routing form details"""
    uuid = form_uri.split("/")[-1]
    response = client.get(f"/routing_forms/{uuid}")
    return response.get("resource", {})


def list_routing_form_submissions(
    form_uri: str,
    sort: str = "created_at:desc",
) -> list:
    """List routing form submissions"""
    params = {
        "routing_form": form_uri,
        "sort": sort,
    }
    return client.paginate("/routing_form_submissions", params=params)


def get_routing_form_submission(submission_uri: str) -> dict:
    """Get routing form submission details"""
    uuid = submission_uri.split("/")[-1]
    response = client.get(f"/routing_form_submissions/{uuid}")
    return response.get("resource", {})


def build_scheduling_url(
    base_url: str,
    name: str = None,
    email: str = None,
    utm_source: str = None,
    utm_medium: str = None,
    utm_campaign: str = None,
    custom_answers: dict = None,
) -> str:
    """Build a pre-filled scheduling URL

    custom_answers: {"a1": "answer1", "a2": "answer2"} for custom questions
    """
    from urllib.parse import urlencode, urlparse, parse_qs, urlunparse

    params = {}

    if name:
        params["name"] = name
    if email:
        params["email"] = email
    if utm_source:
        params["utm_source"] = utm_source
    if utm_medium:
        params["utm_medium"] = utm_medium
    if utm_campaign:
        params["utm_campaign"] = utm_campaign
    if custom_answers:
        params.update(custom_answers)

    if not params:
        return base_url

    parsed = urlparse(base_url)
    query = urlencode(params)

    return urlunparse((
        parsed.scheme,
        parsed.netloc,
        parsed.path,
        parsed.params,
        query,
        parsed.fragment,
    ))


def generate_interview_links(
    event_type_uri: str,
    candidates: list,
) -> list:
    """Generate single-use interview links for candidates

    candidates: [{"name": "John", "email": "john@example.com"}, ...]
    """
    event_type = get_event_type(event_type_uri)
    base_url = event_type["scheduling_url"]

    links = []
    for candidate in candidates:
        # Create single-use link
        link = create_single_use_link(event_type_uri, max_event_count=1)

        # Build pre-filled URL
        scheduling_url = build_scheduling_url(
            base_url=link["booking_url"],
            name=candidate.get("name"),
            email=candidate.get("email"),
            utm_source="interview",
            utm_campaign=candidate.get("campaign", "hiring"),
        )

        links.append({
            "candidate": candidate,
            "link_uri": link["uri"],
            "scheduling_url": scheduling_url,
        })

    return links


from event_types import get_event_type


if __name__ == "__main__":
    from event_types import list_event_types

    # Get an event type
    event_types = list_event_types()
    if event_types:
        et = event_types[0]

        # Build pre-filled URL
        url = build_scheduling_url(
            base_url=et["scheduling_url"],
            name="Jane Doe",
            email="jane@example.com",
            utm_source="email",
            utm_campaign="q1-outreach",
        )
        print(f"Pre-filled URL: {url}")

        # Create single-use link
        single_use = create_single_use_link(et["uri"])
        print(f"Single-use booking URL: {single_use['booking_url']}")

Integration Examples

Slack Notification Integration

# slack_integration.py
# ABOUTME: Notify Slack when Calendly events are scheduled
# ABOUTME: Webhook handler with Slack notifications

import os
import requests
from flask import Flask, request, jsonify
from webhooks import WebhookHandler, verify_webhook_signature

app = Flask(__name__)
webhook = WebhookHandler(signing_key=os.environ.get("CALENDLY_WEBHOOK_SECRET"))

SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL")


def send_slack_notification(message: dict):
    """Send a message to Slack"""
    requests.post(SLACK_WEBHOOK_URL, json=message)


@webhook.on("invitee.created")
def handle_new_booking(data: dict) -> dict:
    """Notify Slack of new booking"""
    invitee = data.get("invitee", {})
    event = data.get("scheduled_event", {})
    event_type = data.get("event_type", {})

    # Extract custom answers
    answers = {}
    for qa in invitee.get("questions_and_answers", []):
        answers[qa["question"]] = qa["answer"]

    # Send Slack notification
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": ":calendar: New Meeting Scheduled",
            },
        },
        {
            "type": "section",
            "fields": [
                {"type": "mrkdwn", "text": f"*Event:*\n{event_type.get('name')}"},
                {"type": "mrkdwn", "text": f"*Invitee:*\n{invitee.get('name')}"},
                {"type": "mrkdwn", "text": f"*Email:*\n{invitee.get('email')}"},
                {"type": "mrkdwn", "text": f"*Time:*\n{event.get('start_time')}"},
            ],
        },
    ]

    if answers:
        answer_text = "\n".join(f"*{q}:* {a}" for q, a in answers.items())
        blocks.append({
            "type": "section",
            "text": {"type": "mrkdwn", "text": f"*Responses:*\n{answer_text}"},
        })

    blocks.append({
        "type": "actions",
        "elements": [
            {
                "type": "button",
                "text": {"type": "plain_text", "text": "View in Calendly"},
                "url": f"https://calendly.com/app/scheduled_events/{event['uri'].split('/')[-1]}",
            },
        ],
    })

    send_slack_notification({"blocks": blocks})

    return {"handled": True, "notified": "slack"}


@webhook.on("invitee.canceled")
def handle_cancellation(data: dict) -> dict:
    """Notify Slack of cancellation"""
    invitee = data.get("invitee", {})
    event = data.get("scheduled_event", {})
    cancellation = invitee.get("cancellation", {})

    send_slack_notification({
        "blocks": [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": ":x: Meeting Canceled",
                },
            },
            {
                "type": "section",
                "fields": [
                    {"type": "mrkdwn", "text": f"*Event:*\n{event.get('name')}"},
                    {"type": "mrkdwn", "text": f"*Invitee:*\n{invitee.get('name')}"},
                    {"type": "mrkdwn", "text": f"*Reason:*\n{cancellation.get('reason', 'Not provided')}"},
                    {"type": "mrkdwn", "text": f"*Canceled by:*\n{cancellation.get('canceled_by')}"},
                ],
            },
        ],
    })

    return {"handled": True, "notified": "slack"}


@app.route("/webhooks/calendly", methods=["POST"])
def calendly_webhook():
    """Handle Calendly webhook"""
    # Verify signature
    signature = request.headers.get("Calendly-Webhook-Signature")
    if signature:
        signing_key = os.environ.get("CALENDLY_WEBHOOK_SECRET")
        if not verify_webhook_signature(request.data, signature, signing_key):
            return jsonify({"error": "Invalid signature"}), 401

    payload = request.json
    result = webhook.handle(payload)
    return jsonify(result)


if __name__ == "__main__":
    app.run(port=8080)

GitHub Actions Integration

# .github/workflows/calendly-sync.yml
name: Sync Calendly Events

on:
  schedule:
    - cron: '0 8 * * *'  # Daily at 8 AM
  workflow_dispatch:

jobs:
  sync-events:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install requests

      - name: Fetch upcoming events
        env:
          CALENDLY_API_KEY: ${{ secrets.CALENDLY_API_KEY }}
        run: |
          python << 'EOF'
          import os
          import requests
          from datetime import datetime, timedelta
          import json

          API_KEY = os.environ["CALENDLY_API_KEY"]
          BASE_URL = "https://api.calendly.com"

          headers = {
              "Authorization": f"Bearer {API_KEY}",
              "Content-Type": "application/json",
          }

          # Get current user
          user_response = requests.get(f"{BASE_URL}/users/me", headers=headers)
          user = user_response.json()["resource"]
          user_uri = user["uri"]

          # Get upcoming events
          now = datetime.utcnow()
          end = now + timedelta(days=7)

          params = {
              "user": user_uri,
              "min_start_time": now.isoformat() + "Z",
              "max_start_time": end.isoformat() + "Z",
              "status": "active",
          }

          events_response = requests.get(
              f"{BASE_URL}/scheduled_events",
              headers=headers,
              params=params,
          )
          events = events_response.json()["collection"]

          print(f"Found {len(events)} upcoming events")

          # Save to file
          with open("upcoming_events.json", "w") as f:
              json.dump(events, f, indent=2)

          # Create summary
          summary = []
          for event in events:
              summary.append({
                  "name": event["name"],
                  "start_time": event["start_time"],
                  "status": event["status"],
              })

          with open("events_summary.json", "w") as f:
              json.dump(summary, f, indent=2)

          print("Events synced successfully")
          EOF

      - name: Upload events artifact
        uses: actions/upload-artifact@v4
        with:
          name: calendly-events
          path: |
            upcoming_events.json
            events_summary.json

Best Practices

1. Rate Limiting

# Rate limit handling
import time
from functools import wraps

def rate_limit_handler(max_retries=3, base_delay=1):
    """Decorator for handling Calendly rate limits"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if "429" in str(e):
                        delay = base_delay * (2 ** attempt)
                        print(f"Rate limited, waiting {delay}s...")
                        time.sleep(delay)
                    else:
                        raise
            raise Exception("Max retries exceeded")
        return wrapper
    return decorator

2. Token Management

# Secure token management
import os
from functools import lru_cache

@lru_cache()
def get_calendly_client():
    """Get cached Calendly client with secure token"""
    token = os.environ.get("CALENDLY_API_KEY")
    if not token:
        raise ValueError("CALENDLY_API_KEY not set")
    return CalendlyClient(api_key=token)

# Never log tokens
def redact_token(text: str) -> str:
    token = os.environ.get("CALENDLY_API_KEY", "")
    if token and token in text:
        return text.replace(token, "[REDACTED]")
    return text

3. Webhook Security

# Webhook signature verification
def verify_and_process_webhook(request):
    """Verify webhook signature before processing"""
    signature = request.headers.get("Calendly-Webhook-Signature")

    if not signature:
        return {"error": "Missing signature"}, 401

    signing_key = os.environ.get("CALENDLY_WEBHOOK_SECRET")
    if not verify_webhook_signature(request.data, signature, signing_key):
        return {"error": "Invalid signature"}, 401

    # Process webhook
    return process_webhook(request.json)

4. Error Handling

# Comprehensive error handling
class CalendlyError(Exception):
    """Base Calendly API error"""
    pass

class RateLimitError(CalendlyError):
    """Rate limit exceeded"""
    def __init__(self, retry_after: int):
        self.retry_after = retry_after
        super().__init__(f"Rate limited. Retry after {retry_after}s")

class NotFoundError(CalendlyError):
    """Resource not found"""
    pass

def handle_api_error(response):
    """Handle API error responses"""
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 60))
        raise RateLimitError(retry_after)
    elif response.status_code == 404:
        raise NotFoundError(response.json().get("message"))
    else:
        response.raise_for_status()

Troubleshooting

Common Issues

Issue: 401 Unauthorized

# Verify token is valid
def verify_token(token: str) -> bool:
    response = requests.get(
        "https://api.calendly.com/users/me",
        headers={"Authorization": f"Bearer {token}"}
    )
    return response.status_code == 200

Issue: No events returned

# Check time range format
from datetime import datetime

def format_time_for_api(dt: datetime) -> str:
    """Format datetime for Calendly API (ISO 8601 with Z)"""
    return dt.strftime("%Y-%m-%dT%H:%M:%SZ")

# Ensure UTC timezone
start = datetime.utcnow()
formatted = format_time_for_api(start)

Issue: Webhook not receiving events

# Verify webhook subscription
def check_webhook_status(webhook_uri: str):
    webhook = get_webhook_subscription(webhook_uri)
    print(f"Status: {webhook.get('state')}")
    print(f"Events: {webhook.get('events')}")
    print(f"URL: {webhook.get('callback_url')}")

    # Verify URL is accessible
    import requests
    try:
        response = requests.post(webhook["callback_url"], json={"test": True})
        print(f"URL accessible: {response.status_code < 500}")
    except Exception as e:
        print(f"URL not accessible: {e}")

Debug Commands

# Test API authentication
curl -X GET "https://api.calendly.com/users/me" \
  -H "Authorization: Bearer $CALENDLY_API_KEY"

# List event types
curl -X GET "https://api.calendly.com/event_types?user=$(curl -s -X GET https://api.calendly.com/users/me -H "Authorization: Bearer $CALENDLY_API_KEY" | jq -r '.resource.uri')" \
  -H "Authorization: Bearer $CALENDLY_API_KEY"

# List webhooks
curl -X GET "https://api.calendly.com/webhook_subscriptions?organization=$(curl -s -X GET https://api.calendly.com/users/me -H "Authorization: Bearer $CALENDLY_API_KEY" | jq -r '.resource.current_organization')" \
  -H "Authorization: Bearer $CALENDLY_API_KEY"

Version History

Version Date Changes
1.0.0 2026-01-17 Initial release with comprehensive Calendly API v2 patterns

Resources


This skill provides production-ready patterns for Calendly scheduling automation, enabling seamless meeting coordination and booking workflows.

Weekly Installs
13
First Seen
Jan 24, 2026
Installed on
claude-code11
trae10
antigravity10
codex10
windsurf10
gemini-cli10