Automating Call Scheduling with Claude API and Calendly in C#
The Problem
You’re receiving dozens of emails a day, and many of them end with “When can we schedule a call?” or “Are you free this week?” Responding to each one means checking your calendar, finding available slots, writing a reply, and then confirming once they pick a time. It’s repetitive, time-consuming, and easy to let slip.
In this post, I’ll show how to combine Claude’s natural language understanding with the Calendly API to fully automate this workflow: Claude reads the email, recognizes scheduling intent, and the system fetches real calendar availability and generates a complete reply — or books the meeting outright when someone confirms a time.
Architecture Overview
1. Recruiter emails: “Can we schedule a call this week?”
2. Claude detects scheduling intent → sets ACTION to CHECK_AVAILABILITY
3. System queries Calendly API for real available timeslots
4. Reply is sent with actual availability baked in
5. Recruiter replies: “Let’s do Tuesday at 10 AM”
6. Claude detects time confirmation → sets ACTION to SCHEDULE_MEETING
7. System finds the closest Calendly slot and generates a booking link
8. Reply is sent with the booking link — meeting is scheduled
Prerequisites
- .NET 8.0 or later
- Anthropic NuGet package for Claude API
- A Calendly account with an API token (from calendly.com/integrations)
- Your Calendly User URI (available via the Calendly API)
Step 1: Set Up the Calendly Service
First, we create a service that wraps the Calendly API. It needs to do two things: fetch available timeslots, and generate a booking link for a specific time.
using System.Text;
using System.Text.Json;
public class CalendlyService
{
private readonly string _apiToken;
private readonly string _userUri;
public CalendlyService(string apiToken, string userUri)
{
_apiToken = apiToken;
_userUri = userUri;
}
}
Step 2: Fetch Available Timeslots
The Calendly API provides an event_type_available_times endpoint that returns real open slots based on your calendar. We query it for the next 7 days and format the results into a human-readable list:
public async Task<string> GetAvailability()
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(“Bearer”, _apiToken);
// Step 1: Get your active event types
var eventTypesResp = await http.GetStringAsync(
$”https://api.calendly.com/event_types?user={Uri.EscapeDataString(_userUri)}&active=true”);
var eventTypesDoc = JsonDocument.Parse(eventTypesResp);
var eventTypes = eventTypesDoc.RootElement.GetProperty(“collection”);
if (eventTypes.GetArrayLength() == 0)
return “I am available Monday-Friday, 9 AM – 5 PM ET.”;
string eventTypeUri = eventTypes[0].GetProperty(“uri”).GetString()!;
// Step 2: Query available times for the next 7 days
string startTime = DateTime.UtcNow.AddHours(1).ToString(“yyyy-MM-ddTHH:mm:ssZ”);
string endTime = DateTime.UtcNow.AddDays(7).ToString(“yyyy-MM-ddTHH:mm:ssZ”);
var availResp = await http.GetStringAsync(
$”https://api.calendly.com/event_type_available_times” +
$”?event_type={Uri.EscapeDataString(eventTypeUri)}” +
$”&start_time={startTime}&end_time={endTime}”);
var availDoc = JsonDocument.Parse(availResp);
var slots = availDoc.RootElement.GetProperty(“collection”);
// Step 3: Filter to business hours and group by day
var eastern = TimeZoneInfo.FindSystemTimeZoneById(“Eastern Standard Time”);
var slotsByDay = new Dictionary<string, List<string>>();
foreach (var slot in slots.EnumerateArray())
{
string startStr = slot.GetProperty(“start_time”).GetString()!;
var dtUtc = DateTime.Parse(startStr, null,
System.Globalization.DateTimeStyles.RoundtripKind);
var dtLocal = TimeZoneInfo.ConvertTimeFromUtc(dtUtc, eastern);
// Skip weekends and outside business hours
if (dtLocal.DayOfWeek == DayOfWeek.Saturday ||
dtLocal.DayOfWeek == DayOfWeek.Sunday) continue;
if (dtLocal.Hour < 9 || dtLocal.Hour >= 17) continue;
string day = dtLocal.ToString(“dddd, MMMM d”);
string time = dtLocal.ToString(“h:mm tt”);
if (!slotsByDay.ContainsKey(day))
slotsByDay[day] = new List<string>();
slotsByDay[day].Add(time);
}
// Step 4: Format as readable text
var sb = new StringBuilder();
sb.AppendLine(“Here are my available timeslots (Eastern Time):”);
sb.AppendLine();
foreach (var day in slotsByDay)
{
// Show a few representative times per day, not every 15-min slot
var times = day.Value;
var picks = new List<string> { times[0] };
if (times.Count > 2) picks.Add(times[times.Count / 2]);
if (times.Count > 1) picks.Add(times[^1]);
sb.AppendLine($” {day.Key}: {string.Join(“, “, picks.Distinct())}”);
}
return sb.ToString();
}
Step 3: Generate a Booking Link
When someone confirms a specific time (“Let’s do Tuesday at 10 AM”), we find the closest available Calendly slot and return its scheduling URL:
public async Task<string> GetBookingLink(string meetingTimeStr, string inviteeEmail)
{
var eastern = TimeZoneInfo.FindSystemTimeZoneById(“Eastern Standard Time”);
if (!DateTime.TryParse(meetingTimeStr, out var requestedET))
return “Please book your preferred time at your convenience.”;
var requestedUtc = TimeZoneInfo.ConvertTimeToUtc(requestedET, eastern);
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(“Bearer”, _apiToken);
// Get event type
var eventTypesResp = await http.GetStringAsync(
$”https://api.calendly.com/event_types?user={Uri.EscapeDataString(_userUri)}&active=true”);
var eventTypesDoc = JsonDocument.Parse(eventTypesResp);
var eventTypes = eventTypesDoc.RootElement.GetProperty(“collection”);
if (eventTypes.GetArrayLength() == 0)
return “No event types available.”;
string eventTypeUri = eventTypes[0].GetProperty(“uri”).GetString()!;
// Search for slots near the requested time (+/- 1 day window)
string startTime = requestedUtc.AddDays(-1).ToString(“yyyy-MM-ddTHH:mm:ssZ”);
string endTime = requestedUtc.AddDays(1).ToString(“yyyy-MM-ddTHH:mm:ssZ”);
var availResp = await http.GetStringAsync(
$”https://api.calendly.com/event_type_available_times” +
$”?event_type={Uri.EscapeDataString(eventTypeUri)}” +
$”&start_time={startTime}&end_time={endTime}”);
var availDoc = JsonDocument.Parse(availResp);
var slots = availDoc.RootElement.GetProperty(“collection”);
// Find the slot closest to the requested time
string? bestUrl = null;
double bestDiff = double.MaxValue;
foreach (var slot in slots.EnumerateArray())
{
var slotUtc = DateTime.Parse(
slot.GetProperty(“start_time”).GetString()!,
null, System.Globalization.DateTimeStyles.RoundtripKind);
double diff = Math.Abs((slotUtc – requestedUtc).TotalMinutes);
if (diff < bestDiff)
{
bestDiff = diff;
bestUrl = slot.GetProperty(“scheduling_url”).GetString();
}
}
if (bestUrl == null || bestDiff > 60)
return “I couldn’t find an available slot at that time.”;
// Pre-fill the invitee’s email in the booking link
if (!string.IsNullOrEmpty(inviteeEmail))
bestUrl += $”?email={Uri.EscapeDataString(inviteeEmail)}”;
return $”Please confirm the meeting by clicking here:\n{bestUrl}”;
}
Step 4: Teach Claude About Scheduling
The magic is in the system prompt. We extend our email reply prompt with scheduling-specific actions and placeholder tokens that the system replaces with real data:
string systemPrompt = “””
You are an email reply assistant that can also handle scheduling.
When someone asks about availability, scheduling a call, or
wants to set up a meeting:
– Set ACTION to CHECK_AVAILABILITY
– Write your reply with a [AVAILABILITY] placeholder where
the timeslots should appear
When someone confirms a specific time (e.g. “Tuesday at 10 AM”,
“Let’s do 2 PM tomorrow”):
– Set ACTION to SCHEDULE_MEETING
– Set MEETING_TIME to the confirmed date/time (yyyy-MM-dd HH:mm)
– Set INVITEE_EMAIL to the sender’s email address
– Write your reply with a [BOOKING_LINK] placeholder
Respond in this format:
ACTION: REPLY | CHECK_AVAILABILITY | SCHEDULE_MEETING | SKIP
MEETING_TIME: <yyyy-MM-dd HH:mm – only for SCHEDULE_MEETING>
INVITEE_EMAIL: <sender email – only for SCHEDULE_MEETING>
REPLY: <the reply body text>
“””;
Step 5: Wire It All Together
After getting Claude’s response, we check the action and inject real Calendly data:
// Parse Claude’s structured response
var parsed = ParseResponse(claudeReply);
string replyBody = parsed.ReplyBody;
// Handle scheduling actions
if (parsed.Action == “CHECK_AVAILABILITY”)
{
// Fetch real timeslots from Calendly
string availability = await calendlyService.GetAvailability();
// Replace the placeholder with actual availability
replyBody = replyBody.Replace(“[AVAILABILITY]”, availability);
}
if (parsed.Action == “SCHEDULE_MEETING”
&& !string.IsNullOrEmpty(parsed.MeetingTime))
{
// Generate a booking link for the confirmed time
string inviteeEmail = parsed.InviteeEmail ?? ExtractEmail(email.From);
string bookingLink = await calendlyService.GetBookingLink(
parsed.MeetingTime, inviteeEmail);
// Replace the placeholder with the actual booking link
replyBody = replyBody.Replace(“[BOOKING_LINK]”, bookingLink);
}
Step 6: The Complete Conversation in Action
Here’s what a real multi-turn conversation looks like with this system:
Email 1 — Initial inquiry
From: recruiter@example.com
Subject: Senior Developer Position
Hi, we have an exciting opportunity. Can we schedule a call to discuss?
Claude detects “schedule a call” → ACTION: CHECK_AVAILABILITY
Auto-generated Reply
Hi Jane,
Thank you for reaching out! I’d be happy to discuss the opportunity.
Here are my available timeslots (Eastern Time):
Monday, April 7: 9:00 AM, 12:00 PM, 4:30 PM
Tuesday, April 8: 10:00 AM, 1:00 PM, 4:00 PM
Wednesday, April 9: 9:30 AM, 2:00 PM, 4:30 PM
Please let me know what time works best for you.
Best regards,
John
Email 2 — Time confirmed
From: recruiter@example.com
Subject: Senior Developer Position
Tuesday at 10 AM works for me!
Claude detects time confirmation → ACTION: SCHEDULE_MEETING, MEETING_TIME: 2026-04-08 10:00
Auto-generated Reply
Hi Jane,
Tuesday at 10:00 AM ET works perfectly.
Please confirm the meeting by clicking here: https://calendly.com/d/abc123?email=recruiter@example.com
You will receive a calendar invitation shortly after confirming.
Looking forward to speaking with you!
Best regards,
John
Parsing Claude’s Multi-Field Response
Since we added new fields (MEETING_TIME, INVITEE_EMAIL), the parser needs to handle them:
static (string Action, string ReplyBody, string MeetingTime, string InviteeEmail)
ParseResponse(string claudeReply)
{
string action = “REPLY”, replyBody = “”;
string meetingTime = “”, inviteeEmail = “”;
var lines = claudeReply.Split(‘\n’);
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i].Trim();
if (line.StartsWith(“ACTION:”))
action = line[“ACTION:”.Length..].Trim().ToUpperInvariant();
else if (line.StartsWith(“MEETING_TIME:”))
meetingTime = line[“MEETING_TIME:”.Length..].Trim();
else if (line.StartsWith(“INVITEE_EMAIL:”))
inviteeEmail = line[“INVITEE_EMAIL:”.Length..].Trim();
else if (line.StartsWith(“REPLY:”))
{
replyBody = line[“REPLY:”.Length..].Trim();
var remaining = lines.Skip(i + 1)
.Where(l => !l.Trim().StartsWith(“ACTION:”)
&& !l.Trim().StartsWith(“MEETING_TIME:”)
&& !l.Trim().StartsWith(“INVITEE_EMAIL:”));
string rest = string.Join(“\n”, remaining).Trim();
if (!string.IsNullOrEmpty(rest))
replyBody += “\n” + rest;
break;
}
}
return (action, replyBody, meetingTime, inviteeEmail);
}
Tips and Best Practices
1. Always Use Real Calendar Data
Never let Claude guess your availability. The AI writes the natural language reply; the system injects real timeslots. This prevents double-bookings and stale information.
2. Pre-fill the Invitee Email
Appending ?email=… to the Calendly scheduling URL saves the other party from typing their email again. Small UX wins like this make automated replies feel polished.
3. Filter Slots for Readability
Calendly returns every available 15-minute slot. Showing all of them in an email is overwhelming. Pick 2-3 representative times per day (morning, midday, afternoon) for a natural-looking reply.
4. Handle Time Zone Conversions
Calendly returns UTC times. Convert to the sender’s expected time zone before including them in the reply. In our example, we convert to Eastern Time since that’s where the user is based.
5. Use a Tolerance Window for Booking
When someone says “Tuesday at 10,” they might mean 10:00 or 10:15 or 10:30. Search for the closest available slot within a reasonable window (we use 60 minutes) rather than requiring an exact match.
6. Add a Fallback
If the Calendly API is down or returns no slots, fall back to a generic message like “I’m available Monday through Friday, 9-5 ET” rather than failing silently.
Conclusion
By combining Claude’s ability to understand conversational context with Calendly’s scheduling API, you can build a system that handles the entire meeting-booking flow automatically. Claude handles the “what should I say” part, while the Calendly integration handles the “when am I actually free” part. The placeholder pattern ([AVAILABILITY] and [BOOKING_LINK]) keeps these concerns cleanly separated.
This approach scales to any scheduling tool with an API — Google Calendar, Microsoft Bookings, Cal.com — by swapping out the CalendlyService implementation while keeping the Claude integration and prompt structure unchanged.