DEV Community

the bomber
the bomber

Posted on

Booking Calls from a Chatbot with Cal.com API v2 – Open Slots, Create, Cancel

Booking Calls from a Chatbot with Cal.com API v2 – Open Slots, Create, Cancel

I built a tiny automation server at home that lets the chatbot on my Dream Futures Tech website schedule, confirm, and cancel phone calls on the fly. The whole flow runs on a Node.js micro‑service that talks to Cal.com’s v2 API. Below is the step‑by‑step code I use, plus a few practical tips for anyone who wants to replicate it for their own small‑business site.


1. Why Cal.com?

  • Self‑hosted – I can run the scheduler on my own server, keeping data under my control.
  • Simple REST API – v2 exposes endpoints for availability, bookings, and cancellations without needing a heavy SDK.
  • Free tier – Enough for a handful of daily appointments, which is perfect for a solo‑owner business.

2. Setting up the API client

I keep the client thin: a small wrapper around fetch that injects the API key and handles JSON conversion.

// apiClient.js – tiny wrapper for Cal.com v2
const BASE_URL = "https://api.cal.com/v2";
const API_KEY = process.env.CALCOM_API_KEY; // set in .env on my home server

export async function calRequest(path, method = "GET", body = null) {
  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${API_KEY}`,
  };

  const opts = { method, headers };
  if (body) opts.body = JSON.stringify(body);

  const res = await fetch(`${BASE_URL}${path}`, opts);
  if (!res.ok) {
    const err = await res.text();
    throw new Error(`Cal.com error ${res.status}: ${err}`);
  }
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

Tip: Store the API key in an environment variable and never commit it to Git. My home server runs Docker, so I inject the variable at container start.

3. Step 1 – Fetch open slots

The chatbot needs to know which times are free before it can suggest anything. Cal.com’s /availability endpoint returns a list of start/end timestamps for a given event type.

// getSlots.js – returns an array of human‑readable slots
import { calRequest } from "./apiClient.js";

export async function getOpenSlots(eventTypeId, daysAhead = 7) {
  const now = new Date().toISOString();
  const end = new Date();
  end.setDate(end.getDate() + daysAhead);

  const data = await calRequest(
    "/availability",
    "POST",
    {
      eventTypeId,
      start: now,
      end: end.toISOString(),
      // optional: limit to 30‑minute slots
      slotInterval: 30,
    }
  );

  // Transform to a simple string array for the chatbot UI
  return data.slots.map((s) => `${new Date(s.start).toLocaleString()} – ${new Date(s.end).toLocaleTimeString()}`);
}
Enter fullscreen mode Exit fullscreen mode

When the chatbot receives a user request like “When can we talk?” I call getOpenSlots and feed the result back as quick‑reply buttons.

4. Step 2 – Create a booking

Once the user picks a slot, I need to turn that choice into a real booking. The /bookings endpoint creates the event and returns a confirmation URL.

// createBooking.js
import { calRequest } from "./apiClient.js";

export async function createBooking(eventTypeId, start, end, customer) {
  const payload = {
    eventTypeId,
    start,
    end,
    // Cal.com expects a "participants" array; I only need the lead here
    participants: [
      {
        email: customer.email,
        name: customer.name,
        // optional: custom fields like phone number
        customInputs: {
          phone: customer.phone,
        },
      },
    ],
  };

  const result = await calRequest("/bookings", "POST", payload);
  return {
    bookingId: result.id,
    confirmationUrl: result.confirmationUrl,
  };
}
Enter fullscreen mode Exit fullscreen mode

The chatbot replies with a friendly message:

"Great, I’ve reserved 2 pm – 2:30 pm on Thursday. Click the link to add it to your calendar: "

I also store bookingId in a tiny SQLite table keyed to the user’s chat ID. That makes cancellation easy later.

5. Step 3 – Cancel a booking

If the prospect says “Can we move that?” I look up the stored bookingId and call the delete endpoint.

// cancelBooking.js
import { calRequest } from "./apiClient.js";

export async function cancelBooking(bookingId) {
  await calRequest(`/bookings/${bookingId}`, "DELETE");
  return true;
}
Enter fullscreen mode Exit fullscreen mode

After the call succeeds I clear the row from SQLite and let the chatbot suggest new slots.

6. Wiring it into the chatbot flow

My site uses Botpress for the conversational UI, but the same ideas apply to Dialogflow, Rasa, or a custom React chat widget.

// botHandler.js – pseudo‑code for the three intents
import { getOpenSlots } from "./getSlots.js";
import { createBooking } from "./createBooking.js";
import { cancelBooking } from "./cancelBooking.js";
import db from "./db.js"; // tiny wrapper around SQLite

// 1⃣ User asks for availability
async function handleAvailability(ctx) {
  const slots = await getOpenSlots("my-event-type-id");
  ctx.reply("Here are the next open times:", { quickReplies: slots });
}

// 2⃣ User selects a slot
async function handleSlotSelection(ctx, slotString) {
  const [startStr, endStr] = slotString.split(" – ");
  const customer = {
    name: ctx.user.name,
    email: ctx.user.email,
    phone: ctx.user.phone,
  };
  const { bookingId, confirmationUrl } = await createBooking(
    "my-event-type-id",
    new Date(startStr).toISOString(),
    new Date(endStr).toISOString(),
    customer
  );
  await db.run("INSERT INTO bookings (chatId, bookingId) VALUES (?, ?)", [ctx.chatId, bookingId]);
  ctx.reply(`All set! Confirm here: ${confirmationUrl}`);
}

// 3⃣ User wants to cancel
async function handleCancel(ctx) {
  const row = await db.get("SELECT bookingId FROM bookings WHERE chatId = ?", [ctx.chatId]);
  if (!row) return ctx.reply("I don’t see any upcoming appointment to cancel.");
  await cancelBooking(row.bookingId);
  await db.run("DELETE FROM bookings WHERE chatId = ?", [ctx.chatId]);
  ctx.reply("Your appointment has been cancelled. Let me know when you’d like to reschedule.");
}
Enter fullscreen mode Exit fullscreen mode

The three intent handlers cover the entire lifecycle. Because each step is a single HTTP request, the latency is under a second on my home broadband, which feels instant to the visitor.

7. Security & rate‑limiting considerations

  • Validate input – The slot string comes from the chatbot UI; I always parse it back to ISO timestamps before sending to Cal.com.
  • Rate limit – I wrap calRequest with a tiny token bucket (max 5 requests/second) to avoid hitting Cal.com’s soft limits.
  • Webhook verification – Cal.com can POST to /webhooks/booking when a meeting is rescheduled or cancelled by the host. I verify the signature header before processing.

8. Deploying on my home server

I run the service in a Docker container on a Raspberry Pi 4. The Dockerfile is a one‑liner:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

server.js just mounts an Express router that exposes /chatbot for Botpress to call. The container restarts automatically via docker-compose if the Pi reboots.


9. Takeaway

Using Cal.com’s v2 API lets a small business turn a generic chatbot into a real appointment‑scheduling assistant with just three HTTP calls: fetch availability → create booking → cancel if needed. The code stays under 150 lines, runs on inexpensive hardware, and gives prospects a frictionless way to lock in a call.

I build sites and automations for small businesses at dreamfuturestech.com.

Top comments (0)