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 like Asia/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

  1. calendar.events scope ≠ freebusy. The freebusy endpoint returned 403 insufficient scopes; I switched to events().list and learned to handle all-day events as whole-day blockers.
  2. Hosting whiplash. Built on PythonAnywhere → discovered the outbound proxy block via curl probes → moved to Render and reworked the deployment for ephemeral disk.
  3. 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.
  4. 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.
  5. A corrupted requirements.txt (requestsopenpyxl merged 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.

Built With

Share this project:

Updates

Submission history