An AI-powered phone receptionist that answers calls 24/7, books appointments on Google Calendar, and sends SMS confirmations — built with Django, Claude AI, Vapi, and PostgreSQL.
- Answers incoming phone calls with a natural-sounding AI voice
- Checks Google Calendar availability in real time
- Books appointments and creates calendar events
- Sends SMS confirmations via Twilio
- Logs every call and appointment to PostgreSQL with a Django admin dashboard
| Component | Role |
|---|---|
| Django | Backend brain — hosts Claude AI, tools, prompts, admin panel |
| Claude API (Anthropic) | AI conversation engine — processes speech, decides actions, calls tools |
| Vapi | Voice pipe — phone numbers, speech-to-text, text-to-speech only |
| PostgreSQL | Call logs and appointment records |
| Google Calendar API | Appointment availability and booking |
| Twilio | SMS confirmations |
Vapi is used only as a voice transport layer (STT/TTS). All AI logic, tools, and prompts live in Django.
[Phone Call] → [Vapi: STT only] → [Django Custom LLM Endpoint]
↓
[Claude AI + Tools]
↙ ↓ ↘
[Google Cal] [Twilio] [PostgreSQL]
↓
[Django Response]
↓
[Vapi: TTS only] → [Caller hears response]
- Caller dials the phone number (via Vapi)
- Vapi converts speech to text and sends it to Django's
/api/vapi/chat/completions/endpoint - Django calls Claude API with the system prompt and tool definitions
- Claude decides what to do — check calendar, book appointment, send SMS, etc.
- Django executes tools locally and returns the final response to Vapi
- Vapi converts the text response back to speech for the caller
git clone https://github.com/codewithmuh/ai-voice-agent.git
cd ai-voice-agent
cp .env.example .envpython3 -c "import secrets; print(secrets.token_urlsafe(50))"Paste the output into your .env file as the SECRET_KEY value.
SECRET_KEY=your-generated-secret-key
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,*
DB_NAME=ai_receptionist
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=db
DB_PORT=5432
ANTHROPIC_API_KEY=sk-ant-...
VAPI_API_KEY=...
GOOGLE_CALENDAR_CREDENTIALS=google-calendar-credentials.json
GOOGLE_CALENDAR_ID=your-calendar-id@group.calendar.google.com
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
TWILIO_PHONE_NUMBER=+1...
Where to get keys:
- Anthropic: console.anthropic.com
- Vapi: dashboard.vapi.ai
- Google Calendar: See detailed setup below
- Twilio: twilio.com/console
- Go to Google Cloud Console
- Click Select a Project → New Project → name it → Create
- Go to APIs & Services → Library
- Search for "Google Calendar API" → click it → Enable
- Go to APIs & Services → Credentials
- Click + Create Credentials → Service Account
- Name it (e.g., "calendar-bot") → Create and Continue → skip optional steps → Done
- Click on the newly created service account → Keys tab → Add Key → Create New Key → JSON → Create
- A
.jsonfile will download — move it to your project root:
mv ~/Downloads/your-downloaded-file.json ./google-calendar-credentials.json- Open Google Calendar
- Click the gear icon in the top-right → Settings
- Under Settings for my calendars on the left, click the calendar you want
- Click Integrate calendar — the Calendar ID is displayed there
- For your default calendar, the Calendar ID is simply your Gmail address (e.g.,
you@gmail.com)
- Open the downloaded JSON file and copy the
client_emailvalue (looks likecalendar-bot@your-project.iam.gserviceaccount.com) - In Google Calendar → Settings and sharing for your calendar
- Under Share with specific people or groups → + Add people and groups
- Paste the service account email → set permission to Make changes to events → Send
Tip: Use the same Google account for both Google Cloud and Google Calendar to keep things simple.
docker compose up --buildThis starts Django on http://localhost:8000 and PostgreSQL on port 5432.
Migrations run automatically on container start. To create a Django admin superuser:
docker compose exec web python manage.py createsuperuser# Test the webhook endpoint
curl http://localhost:8000/api/vapi/webhook/
# Returns: {"status": "ok", "service": "AI Voice Receptionist"}
# Test the custom LLM endpoint
curl -X POST http://localhost:8000/api/vapi/chat/completions/ \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Hi, I want to book an appointment"}]}'
# Returns an OpenAI-format response with Claude's replyInstall ngrok if you don't have it:
# macOS
brew install ngrok
# Or download from https://ngrok.com/downloadStart the tunnel:
ngrok http 8000Copy the https://...ngrok-free.app URL from the output.
- Go to Vapi Dashboard
- Create an Assistant
- Set the Model to
Custom LLMwith the URL:https://your-ngrok-url.ngrok-free.app/api/vapi/chat/completions/ - Set the Server URL (for end-of-call reports) to:
https://your-ngrok-url.ngrok-free.app/api/vapi/webhook/ - Buy or import a phone number and assign it to the assistant
- Call the number to test (or use the web call button in the Vapi dashboard)
Important: The Custom LLM URL is where Vapi sends conversation messages. Django calls Claude, runs tools, and returns the response. Vapi only handles voice (STT/TTS).
ai-voice-agent/
├── receptionist_project/
│ ├── settings.py # Django config
│ └── urls.py # URL routing
├── calls/
│ ├── models.py # CallLog + Appointment models
│ ├── views.py # Custom LLM endpoint + Vapi webhook
│ ├── urls.py # API routes
│ ├── admin.py # Django admin registration
│ ├── agent/
│ │ ├── receptionist.py # Claude agentic loop (the brain)
│ │ ├── tools.py # Tool definitions (calendar, SMS, DB)
│ │ └── prompts.py # System prompt (Bright Smile Dental)
│ └── services/
│ ├── calendar.py # Google Calendar API
│ ├── sms.py # Twilio SMS
│ └── database.py # Call logging
├── docker-compose.yml
├── Dockerfile
├── .env.example
└── requirements.txt
| Endpoint | Purpose |
|---|---|
POST /api/vapi/chat/completions/ |
Custom LLM — receives messages from Vapi, calls Claude + tools, returns response |
POST /api/vapi/webhook/ |
Webhook — receives end-of-call reports, logs calls to DB |
GET /api/vapi/webhook/ |
Health check |
Access the admin panel at http://localhost:8000/admin/ to:
- View all call logs (caller phone, duration, summary, timestamp)
- View all booked appointments (patient name, date, time, type)
- Filter calls by date and booking status
- Filter appointments by type and date
- Search by phone number or patient name
# Railway
railway init
railway add --plugin postgresql
railway upSet your environment variables in the Railway dashboard, then update the Vapi Custom LLM URL and Server URL to your Railway domain.

MIT