This tutorial shows how to build an appointment booking flow where customers schedule meetings via WhatsApp and events appear in your team's calendar.
Architecture overview
The integration connects three systems:
- watsi — receives WhatsApp messages and sends replies via the API
- Your backend — processes booking requests and manages availability
- Calendar provider — Google Calendar, Outlook, Cal.com, or any provider with a booking API
Customer → WhatsApp → watsi webhook → Your backend → Calendar APIStep by step
- 1
Set up a webhook listener
Register a webhook subscription to receive
message.receivedevents. See the webhook tutorial for the full setup walkthrough.curl -X POST https://api.watsi.ai/api/v1/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/watsi", "events": ["message.received"] }' - 2
Detect booking intent
When a message arrives, check whether the customer is asking to book an appointment. You can use keyword matching or an LLM to detect intent.
app.post('/webhooks/watsi', async (req, res) => { const event = req.body const message = event.data.message // Simple keyword detection const bookingKeywords = ['book', 'appointment', 'schedule', 'reserve', 'agendar', 'cita'] const wantsBooking = bookingKeywords.some((kw) => message.body.toLowerCase().includes(kw)) if (wantsBooking) { await handleBookingFlow(event.data) } res.sendStatus(200) }) - 3
Fetch available slots
Query your calendar provider for available time slots. This example uses the Google Calendar FreeBusy API, but the same pattern works with any provider.
async function getAvailableSlots(date) { const calendar = google.calendar({ version: 'v3', auth }) const busy = await calendar.freebusy.query({ requestBody: { timeMin: startOfDay(date).toISOString(), timeMax: endOfDay(date).toISOString(), items: [{ id: CALENDAR_ID }], }, }) // Compute free 30-minute windows from the busy blocks return computeFreeWindows(busy.data, 30) } - 4
Send available times via WhatsApp
Reply to the customer with the available slots. Format times in the customer's timezone when possible.
async function sendAvailableSlots(conversationId, slots) { const formatted = slots .map((s, i) => `${i + 1}. ${formatTime(s.start)} - ${formatTime(s.end)}`) .join('\n') await fetch('https://api.watsi.ai/api/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ conversation_id: conversationId, body: `Here are the available times:\n\n${formatted}\n\nReply with the number to book.`, }), }) } - 5
Confirm the booking
When the customer replies with their selection, create the calendar event and send a confirmation message.
async function confirmBooking(conversationId, slot, customer) { // Create the calendar event await calendar.events.insert({ calendarId: CALENDAR_ID, requestBody: { summary: `Meeting with ${customer.name}`, start: { dateTime: slot.start }, end: { dateTime: slot.end }, attendees: [{ email: customer.email }], }, }) // Send confirmation via watsi await fetch('https://api.watsi.ai/api/v1/messages', { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ conversation_id: conversationId, body: `Your appointment is confirmed for ${formatTime(slot.start)}. See you then!`, }), }) }
Production considerations
- Timezone handling — store and display times in the customer's timezone. Use the contact profile or ask during the flow.
- Concurrency — check availability again before confirming to prevent double-booking when multiple customers select the same slot.
- Cancellations — listen for follow-up messages like “cancel” and update the calendar event accordingly.
- Reminders — use WhatsApp message templates to send appointment reminders outside the 24-hour messaging window.
- State management — track booking flow state per conversation (e.g. awaiting date, awaiting slot selection, confirmed) in your database.