How to Integrate Retell AI With Google Calendar for Booking
Retell AI does not ship a Google Calendar booking toggle, so the integration is a custom function: the agent calls a webhook on your backend mid-conversation, and your backend talks to the Google Calendar API. The demo version of that is a weekend of work. The production version turns on three details the happy path skips. First, latency: the function runs during a live call, so the round trip has to stay near two seconds or the caller hears silence, which means keeping the on-call work small and letting the agent speak while it runs. Second, idempotency: voice tool calls get retried, so the booking has to carry a deterministic event id that turns a duplicate insert into a no-op instead of a double booking. Third, time zones: a caller who says three o'clock means a wall-clock time, so every event needs an explicit IANA time zone or you book the wrong hour. Get those three right and the booking holds on a real phone line.
Published · Updated · Supreet Tare
Anchored in a real build for a US client, anonymized per NDA. The code is illustrative, not client code; the Retell mechanics follow the current docs.
Why Retell has no Google Calendar “booking” button
Retell’s built-in scheduling integration is Cal.com. For Google Calendar there is no equivalent toggle, and that is not an oversight: a booking is not a setting, it is a decision your system makes during a phone call. So you build it as a custom function, the tool-call primitive Retell gives an agent.
The shape is three hops. The agent decides it has enough to book and calls the function. Retell POSTs the function’s arguments to a URL you host. Your backend talks to the Google Calendar API and returns a small JSON result that the agent reads back to the caller.
caller <-> Retell agent -> your backend -> Google Calendar
(custom function) (events API)
Everything interesting lives in that middle hop. Retell handles the conversation; Google stores the event; your backend is the only place that knows the business rules. That is where the production work is, and where the demo quietly cheats.
The happy path, in one afternoon
The demo build is genuinely small. One custom function, one backend handler, one events.insert call.
{
"type": "custom",
"name": "book_appointment",
"description": "Book a callback once the caller confirms a specific date and time.",
"url": "https://api.example.com/retell/book",
"speak_during_execution": true,
"speak_after_execution": true,
"timeout_ms": 3000,
"parameters": {
"type": "object",
"properties": {
"start_time": { "type": "string", "description": "ISO 8601 local time the caller agreed to." },
"name": { "type": "string" }
},
"required": ["start_time", "name"]
}
}
The backend authenticates to Google with a service account (domain-wide delegation) or a stored OAuth refresh token, inserts the event, and returns. In the demo it works on the first try, and it is tempting to ship it.
It demos perfectly because a demo is one caller, one call, one time zone, and no retries. A real phone line is none of those things. Three things the happy path ignored start to bite the moment it is live.
1. The booking runs inside a live call (latency)
The function does not run in the background. It runs while the caller is on the line, and Retell holds the conversation open until your webhook returns or the timeout fires. If your handler checks availability, inserts the event, sends a confirmation email, and writes to a CRM all synchronously, the caller sits in silence for the sum of all four, and Google’s API is not always fast.
Two settings keep this natural. Set speak_during_execution so the agent says something (“let me get that booked for you”) instead of going quiet, and set timeout_ms to a hard ceiling a little above your target rather than leaving the default. Then hold the synchronous path to one job: make the booking. Everything that is not the booking (the email, the CRM write, the reminder) goes on an async queue that runs after you return. Target a sub-two-second round trip for the part the caller waits on.
2. The function will be retried (idempotency)
Voice tool calls get retried more than HTTP calls you write by hand. A network blip retries the POST. The model occasionally calls the same tool twice. The caller says “yes, book it” again because they did not hear the confirmation. Each of those can reach your backend, and events.insert mints a fresh random id every time, so each one creates another event. The caller ends up triple-booked and nobody notices until the calendar is a mess.
The fix is a deterministic event id. Google Calendar lets you supply your own id on insert; derive it from the caller and the slot, and a retry inserts the same id, which Google rejects with a 409 you treat as success.
// Retell calls this when the agent decides to book.
// Keep the synchronous path tiny: the caller is waiting on the line.
export async function book(req: BookArgs) {
const tz = "America/Los_Angeles"; // the business's zone, not the server's
const start = DateTime.fromISO(req.start_time, { zone: tz });
const end = start.plus({ minutes: 30 });
// Deterministic id: a retried call inserts the same id, so it is a no-op,
// not a second event. (Google ids are base32hex, 5 to 1024 chars.)
const eventId = base32hex(sha1(`${req.caller_id}:${start.toISO()}`));
try {
await calendar.events.insert({
calendarId: BUSINESS_CALENDAR,
requestBody: {
id: eventId,
summary: `Callback: ${req.name}`,
start: { dateTime: start.toISO(), timeZone: tz },
end: { dateTime: end.toISO(), timeZone: tz },
},
});
} catch (e) {
if (e.code !== 409) throw e; // 409 alreadyExists = already booked = success
}
return { booked: true, when: start.toFormat("ccc d LLL, h:mm a") };
}
The deterministic id handles retries of the same booking. It does not handle two different callers racing for one slot, and that is a separate trap: checking availability with a FreeBusy query and then inserting has a window between the check and the write where a second caller can pass the same check. It is the same check-then-act race we wrote about in rate limiting a voice agent with one Postgres UPDATE, in a different system. You cannot make the whole calendar a single atomic compare-and-set, so guard the slot in your own store with a unique constraint and let the insert happen only after you win that row.
3. “Three o’clock” is a wall-clock time (time zones)
When a caller says “three o’clock,” they mean three in their day, on a wall clock. The agent passes you a time string. If you send Google a dateTime with no timeZone, Google interprets it in the calendar’s default zone, and if your server runs in UTC while the business runs in US Pacific, you have just booked midnight for a 3 p.m. callback. It looks fine in your logs and wrong in the calendar.
Two rules close this. Always send an explicit IANA timeZone on both start and end, as in the snippet above. And resolve the caller’s spoken time to the business zone in your own code, deliberately, rather than trusting the model to do time-zone arithmetic in its head. The model is good at hearing “next Tuesday at three”; it is not the thing you want owning daylight-saving edge cases.
What the production call path actually looks like
Put together, the synchronous path the caller waits on is short and boring, which is the point:
- Validate the arguments and resolve the spoken time to the business zone.
- Win the slot in your own store (unique constraint) so two callers cannot both take it.
- Insert the event with a deterministic id and explicit time zones.
- Return a one-line confirmation the agent can read back.
The email, the CRM record, and the reminder schedule all happen after step 4, off the call path. The caller hears a confirmation in under two seconds; the slower bookkeeping catches up a moment later.
Key takeaways
- Google Calendar booking in Retell is a custom function calling your backend, not a built-in integration. The business rules live in that backend.
- The function runs during a live call. Speak during execution, cap the timeout, and keep the synchronous path to the booking alone.
- Voice tool calls get retried. A deterministic event id turns a retry into a 409 you treat as success instead of a double booking.
- Guard the slot with a unique constraint in your own store; a FreeBusy check followed by an insert is a race, not a lock.
- Send an explicit IANA time zone on every event, and do the time-zone math in code, not in the prompt.
What this means if you are an IT services firm
If a client wants their voice agent to book into Google Calendar, the demo will look done in a week and the production version is mostly the four edge cases above. The questions worth asking your team: what does the caller hear while the booking runs, what happens when the tool call is retried, and whose time zone is the event written in. If those answers are not explicit, the build is a demo, not a deployment. This is the kind of work we do behind IT services firms, under their brand.
Reading this because a client asked for voice AI? That is the conversation we are built for. What taritas does for partners.