Build a Voice Receptionist for Your Business · 50 min · Vapi · Google Calendar API · Twilio
Objective
Vapi tools: your webhook server as the "hands"
Same idea as Course 01's agent tools, higher stakes on timing
Just like Course 01's read_file/web_search tools, a Vapi tool is your own code that Vapi calls when the assistant decides it's needed -- the model can't check a calendar itself, it asks your webhook server to. What's different here: a text agent's tool call can take 5-10 seconds without anyone noticing. A voice agent's tool call happening mid-conversation has the caller sitting in real-time silence, waiting -- Vapi's own guidance is to keep tool responses under roughly 3 seconds for a call to still feel natural.
Checking real google calendar availability
import { google } from 'googleapis';
app.post('/tools/check-availability', async (req, res) => {
const { requestedDate } = req.body.message.toolCalls[0].function.arguments;
const calendar = google.calendar({ version: 'v3', auth: getServiceAccountAuth() });
const freeBusy = await calendar.freebusy.query({
requestBody: {
timeMin: `${requestedDate}T09:00:00Z`,
timeMax: `${requestedDate}T17:00:00Z`,
items: [{ id: process.env.CALENDAR_ID }],
},
});
const busySlots = freeBusy.data.calendars[process.env.CALENDAR_ID!].busy;
const availableSlots = computeOpenSlots(busySlots, requestedDate); // 3 open slots, as strings
res.json({ results: [{ toolCallId: req.body.message.toolCalls[0].id, result: JSON.stringify(availableSlots) }] });
});
Notice the response shape: Vapi expects results keyed by toolCallId, matching the specific tool call it made -- this is the same "match responses to the right call" principle from Course 01's tool_use_id, applied to Vapi's protocol instead of Anthropic's. A Google service account (simpler than full OAuth for this course's context, per the SPEC's own recommendation) needs to be shared on the calendar you're checking.
Booking and SMS: the moment the call becomes a real action
A tool that's slow enough to cause dead air is worse than a tool that fails fast
If your calendar check or SMS send is going to take more than a couple seconds, tell the caller something ("let me check that for you") before the silence starts, or optimize the call itself (a service account query is much faster than an interactive OAuth flow, for instance). A caller experiencing unexplained silence will assume the call dropped, which is a worse outcome than a slightly slower but acknowledged wait.
Your book_appointment tool successfully creates the calendar event but the Twilio SMS send fails silently (bad phone number format from the caller). The call continues and the assistant tells the caller "you're all set, confirmation sent!" What's wrong with this, and what should happen instead?
The calendar booking succeeding doesn't mean the SMS succeeded — they're two separate tool calls, and claiming success on one because the other worked is exactly the kind of unverifiable claim this whole curriculum keeps warning against (Course 01's test-driven loop, Course 06's "don't trust the model's own success claim"). The webhook should report SMS failure back to the assistant so it can honestly tell the caller "you're booked, but I couldn't text a confirmation — is your number definitely [repeat it back]?"
What you're building
webhook_server.ts: check_availability (Google Calendar freebusy), book_appointment (calendar event creation), and send_confirmation_sms (Twilio) — all wired as Vapi tools, tested with a real live call that books a real appointment and sends a real text, response times measured and kept under Vapi's real-time budget.