Inspiration
Every salesperson knows the gut-punch of a dead lead. Research shows that 50% of leads are lost when follow-up takes longer than 30 minutes — yet most small businesses still follow up by hand, days later. I kept seeing the same pattern: someone fills a form, a salesperson calls back, they play phone tag, the meeting never happens, and the lead quietly goes cold.
I wanted to close that loop: capture a lead, call them immediately, and convert that call into an actual booked appointment — with zero double-bookings and zero manual calendar work. The CALL-E SDK made the voice side real, so the project became a hybrid: AI voice + Google Calendar + Gmail wired into one pipeline.
What I learned
- Timezones are the silent killer of appointments. The agent can't say "Asia/Kolkata" to a customer — it has to say "IST (India Standard Time)." I built a friendly-label map, derived the lead's zone from their phone number via
phonenumbers, and normalized legacy names likeAsia/Calcutta → Asia/Kolkata. The conversion is pure math:
$$t_{\text{company}} = t_{\text{lead}} + \Delta tz, \qquad 15{:}00\ \text{IST} \rightarrow 05{:}30\ \text{EDT}$$
- OAuth on hosted apps is unforgiving. The redirect URI must match Google's registered list character-for-character; even a trailing slash breaks it.
- Free hosting has invisible walls. PythonAnywhere's free tier proxy-blocks arbitrary outbound hosts (my CALL-E API calls died with
403 from proxy after CONNECT), and Render's free instances wipe the filesystem on every restart — so I restore the OAuth token from an environment variable and let the refresh token keep the site permanently connected. - Consent-first calls are better engineering. The agent discloses it's AI, offers only calendar-confirmed free slots, and books nothing unless the lead explicitly confirms a time — a missed or refused call never creates a ghost appointment.
How I built it
A Flask app orchestrates the loop: a lead form (country-code dropdown + phone) → timezone auto-detection → CALL-E outbound call created at runtime with a strict result schema (time_confirmed, preferred_day, preferred_time, timezone) → polling until terminal → calendar insert + Gmail confirmation.
Availability comes from the company's live calendar. With 30-minute slots over a 10 AM–6 PM window there are 16 possible slots, and the conflict check is simple and airtight:
$$\text{slot free} \iff \nexists\ e :\ e_{\text{start}} < s_{\text{end}} \wedge e_{\text{end}} > s_{\text{start}}$$
The server re-checks this immediately before inserting, so a race can't double-book. Batch upload runs the same pipeline over an Excel file, adds a status column, and lets you re-upload only the leads that didn't connect. Tests run without dialing anyone — a fake calendar service drives the slot logic, and preview mode needs no credentials.
Challenges I faced
calendar.eventsscope ≠freebusy. The freebusy endpoint returned403 insufficient scopes; I switched toevents().listand learned to handle all-day events as whole-day blockers.- Hosting whiplash. Built on PythonAnywhere → discovered the outbound proxy block via curl probes → moved to Render and reworked the deployment for ephemeral disk.
- Burning free call credits. With ~16 calls left, every live test mattered. One unanswered test call proved the safety path works: no confirmation, no booking.
- JS apostrophes inside a Python triple-quoted HTML string — the classic escaping trap that silently corrupted the demo page until I checked the served JS.
- A corrupted
requirements.txt(requestsopenpyxlmerged into one line) broke the server build until I caught it.
The result: a lead goes from form submission to a confirmed, timezone-correct appointment in under a minute — powered by CALL-E's voice, Google's calendar, and a whole lot of edge-case discipline.
Log in or sign up for Devpost to join the conversation.