Adding Call Booking to My Site Chatbot Using the Cal.com API v2
I am Kaleb, a web developer running Dream Futures Tech here in Pompano Beach, Florida. On my own business website, I run a custom chatbot to handle initial questions from prospective leads. For a long time, the bot only answered text queries or told people to email me. That felt like a bottleneck.

I am Kaleb, a web developer running Dream Futures Tech here in Pompano Beach, Florida. On my own business website, I run a custom chatbot to handle initial questions from prospective leads. For a long time, the bot only answered text queries or told people to email me. That felt like a bottleneck. I wanted visitors to be able to check my availability, book a discovery call right inside the chat window, and even cancel if their schedule changed, without ever leaving the page.
To build this, I integrated the Cal.com API v2 into my backend services running on my home automation server. In my judgment, keeping users inside the conversational flow rather than redirecting them to an external scheduling link noticeably improves completion rates. Here is how I put it together using Node.js and the v2 endpoints.
Before writing any chat logic, you need an API key from your Cal.com account and your target event type ID. Cal.com’s v2 API uses Bearer token authentication. I store my API key and event type ID in my environment variables.
const CAL_API_KEY = process. env. CAL_API_KEY; const EVENT_TYPE_ID = process. env. CAL_EVENT_TYPE_ID; const CAL_BASE_URL = ' https://api.cal.com/v2 '; const headers = { ' Authorization ': `Bearer ${ CAL_API_KEY } `, ' Content-Type ': ' application/json ', ' cal-api-version ': ' 2024-06-14 ' }; Enter fullscreen mode Exit fullscreen mode It is important to include the cal-api-version header since the v2 API requires explicit versioning. Without it, your requests will fail or return unexpected deprecation warnings.
When a visitor asks the chatbot when we can talk, the bot triggers a backend function to fetch available slots for the upcoming week. The v2 slots endpoint requires a start time and an end time in ISO format.
async function getOpenSlots () { const startTime = new Date (). toISOString (); const endDate = new Date (); endDate. setDate ( endDate. getDate () + 7 ); const endTime = endDate. toISOString (); const url = ` ${ CAL_BASE_URL } /slots/available?eventTypeId= ${ EVENT_TYPE_ID } &startTime= ${ startTime } &endTime= ${ endTime } `; const response = await fetch ( url, { method: ' GET ', headers }); const data = await response. json (); // Return a simplified array of slot times for the chat UI return data. data. slots. map ( slot => slot. start ); } Enter fullscreen mode Exit fullscreen mode The chatbot receives this array and formats the top three or four choices into clickable buttons for the user. This keeps the chat interface clean instead of dumping a massive list of timestamps into the text stream.
Once the user clicks a specific time slot and provides their name and email address through the chat prompts, the bot sends a POST request to create the booking.
async function bookCall ( slotStartTime, attendeeName, attendeeEmail ) { const payload = { eventTypeId: Number ( EVENT_TYPE_ID ), start: slotStartTime, attendee: { name: attendeeName, email: attendeeEmail, timeZone: ' America/New_York ' } }; const response = await fetch ( ` ${ CAL_BASE_URL } /bookings`, { method: ' POST ', headers, body: JSON. stringify ( payload ) }); const data = await response. json (); return data. data; } Enter fullscreen mode Exit fullscreen mode Upon a successful response, I grab the booking UID returned by the API. I save this UID in the active chat session state so the user has immediate access to it if they need to make changes.
Plans change often. If a user tells the chatbot they need to cancel their scheduled call, the bot asks for their booking UID or checks the active session memory. It then hits the cancellation endpoint.
async function cancelBooking ( bookingUid, cancellationReason = ' Canceled via website chatbot ' ) { const payload = { cancellationReason }; const response = await fetch ( ` ${ CAL_BASE_URL } /bookings/ ${ bookingUid } /cancel`, { method: ' POST ', headers, body: JSON. stringify ( payload ) }); const data = await response. json (); return data. status === ' success '; } Enter fullscreen mode Exit fullscreen mode Once the cancellation returns a success status, the chatbot confirms the cancellation to the user and offers to show open slots again so they can reschedule immediately.
Building this directly into my site chatbot took an afternoon, and it has run reliably ever since. One thing to watch out for is rate limits and error handling; if a user tries to book a slot that someone else took five seconds prior, the API will throw an error. My chatbot handles that edge case by catching the error and asking the user to pick a fresh slot.
I build sites and automations for small businesses at dreamfuturestech.com.