Enable your entire batch/team to interact with your Google Sheet via WhatsApp - no app installation, no learning curve, works on any phone.
β±οΈ Setup time: 60 min π° Cost: ~$5-15/month (Twilio + Claude API + Cloud Run) π₯ Users: Anyone with WhatsApp (public or invite-only)
What you're building
User experience: - Users text your bot number on WhatsApp - They type/voice-note their request naturally - Bot replies with results or confirmation
Perfect for Indian batches where WhatsApp = default channel.
Setup Overview
1Set up Twilio + WhatsApp Sandbox
Twilio provides WhatsApp API access. For testing/personal use, they give a free Sandbox number (no approval needed).
- Sign up at twilio.com (free trial gives $15 credit)
- Verify your phone
- Console β Messaging β Try it out β Send a WhatsApp message
- You'll see a Sandbox number like
+1 415 523 8886and a join code likejoin yellow-tiger - Save these: - Sandbox WhatsApp number - Join code - Your Account SID and Auth Token (from console dashboard)
Users join by:
- Save Sandbox number in contacts
- WhatsApp message it:
join yellow-tiger - They're now allowed to interact with the bot for 72 hours (auto-renews on activity)
2Get Claude API Key
Same as previous options - console.anthropic.com β API Keys β Create.
3Create GCP Service Account
Same as Option 2 Step 3. You need:
- service-account.json file
- Service account email
4Share Sheet with Service Account
Open sheet β Share β paste service account email β Editor β Share.
5Write the Bot
mkdir ~/Desktop/tracker-whatsapp-bot
cd ~/Desktop/tracker-whatsapp-bot
5.1 - main.py
import os
import json
from flask import Flask, request
import anthropic
from google.oauth2 import service_account
from googleapiclient.discovery import build
from twilio.twiml.messaging_response import MessagingResponse
from twilio.rest import Client as TwilioClient
app = Flask(__name__)
# --- Config ---
TWILIO_ACCOUNT_SID = os.environ['TWILIO_ACCOUNT_SID']
TWILIO_AUTH_TOKEN = os.environ['TWILIO_AUTH_TOKEN']
TWILIO_WHATSAPP_NUMBER = os.environ['TWILIO_WHATSAPP_NUMBER'] # e.g., 'whatsapp:+14155238886'
ANTHROPIC_KEY = os.environ['ANTHROPIC_KEY']
SHEET_ID = os.environ['SHEET_ID']
SHEET_TAB = os.environ.get('SHEET_TAB', 'TS-03 Quiz & Assignment')
# Whitelist WhatsApp numbers (e.g., "whatsapp:+919876543210")
ALLOWED_NUMBERS = os.environ['ALLOWED_NUMBERS'].split(',')
# --- Clients ---
claude = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
twilio_client = TwilioClient(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
creds = service_account.Credentials.from_service_account_file(
'/secrets/service-account.json',
scopes=['https://www.googleapis.com/auth/spreadsheets']
)
sheets_api = build('sheets', 'v4', credentials=creds)
# In-memory session state (use Redis for production)
pending_confirmations = {}
# --- System prompt ---
SYSTEM_PROMPT = f"""You are a WhatsApp tracker bot. Parse the user's message into JSON.
VALID SUBJECTS ONLY:
- FoVL-Language
- GPU Programming
- Ethics and Professional Life
- Innovation and IP Management
- Advanced AI
Sheet columns: #, Subject, Type, Title, Due Date (DD-Mmm-YYYY), Time (HH:MM IST),
Mode (default: Google Classroom), Source (default: GCR), Status (default: Upcoming),
Days Left (skip - formula), Notes
Return ONLY valid JSON, no markdown or extra text:
READ: {{"action": "read", "query": "..."}}
ADD: {{"action": "add", "row": {{"subject": ..., "type": ..., "title": ..., "due_date": ..., "time": ..., "notes": ...}}}}
UPDATE: {{"action": "update_status", "row_num": N, "new_status": "Completed"}}
HELP: {{"action": "help"}}
UNKNOWN: {{"action": "unknown", "message": "..."}}
Convert SGT to IST if needed (subtract 2h 30m).
"""
def read_sheet(query):
result = sheets_api.spreadsheets().values().get(
spreadsheetId=SHEET_ID,
range=f"'{SHEET_TAB}'!A4:K100"
).execute()
rows = result.get('values', [])
response = claude.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"Sheet data:\n{json.dumps(rows)}\n\nQuery: {query}\n\nAnswer concisely for WhatsApp (max 1000 chars, use emojis for visual clarity)."
}]
)
return response.content[0].text
def add_row(row_data):
existing = sheets_api.spreadsheets().values().get(
spreadsheetId=SHEET_ID,
range=f"'{SHEET_TAB}'!A:A"
).execute().get('values', [])
next_num = len([r for r in existing if r and r[0].isdigit()]) + 1
values = [[
next_num,
row_data['subject'],
row_data.get('type', 'Assignment'),
row_data.get('title', ''),
row_data.get('due_date', ''),
row_data.get('time', '23:59 IST'),
'Google Classroom',
'GCR',
'Upcoming',
'',
row_data.get('notes', '')
]]
sheets_api.spreadsheets().values().append(
spreadsheetId=SHEET_ID,
range=f"'{SHEET_TAB}'!A5",
valueInputOption='USER_ENTERED',
body={'values': values}
).execute()
return f"β
Added #{next_num}: {row_data['subject']} - {row_data.get('title', '')}"
def update_status(row_num, new_status):
sheet_row = int(row_num) + 4
sheets_api.spreadsheets().values().update(
spreadsheetId=SHEET_ID,
range=f"'{SHEET_TAB}'!I{sheet_row}",
valueInputOption='USER_ENTERED',
body={'values': [[new_status]]}
).execute()
return f"β
Row #{row_num} β {new_status}"
HELP_MESSAGE = """π *TS-03 Tracker Bot*
*Read commands:*
β’ What's due this week?
β’ Show upcoming items
β’ Show all GPU Programming items
*Write commands:*
β’ Add GPU quiz on Oct 15 at 6pm
β’ Add Ethics assignment due Friday
(Then reply YES to confirm)
*Update commands:*
β’ Mark row 5 as completed
β’ Mark Ethics assignment as done
*Other:*
β’ /help - Show this message
β’ /whoami - Show your WhatsApp ID"""
@app.route('/whatsapp', methods=['POST'])
def whatsapp():
incoming_msg = request.values.get('Body', '').strip()
sender = request.values.get('From', '') # 'whatsapp:+91xxx'
resp = MessagingResponse()
msg = resp.message()
# --- Auth check ---
if sender not in ALLOWED_NUMBERS:
msg.body(f"β Unauthorized. Your number: {sender}\n\nAsk admin to whitelist you.")
return str(resp)
# --- Special commands ---
if incoming_msg.lower() in ['/help', 'help']:
msg.body(HELP_MESSAGE)
return str(resp)
if incoming_msg.lower() == '/whoami':
msg.body(f"Your ID: {sender}")
return str(resp)
# --- Confirmation flow ---
if incoming_msg.lower() in ['yes', 'y', 'confirm']:
if sender in pending_confirmations:
result = add_row(pending_confirmations[sender])
del pending_confirmations[sender]
msg.body(result)
else:
msg.body("Nothing pending to confirm.")
return str(resp)
if incoming_msg.lower() in ['no', 'cancel']:
pending_confirmations.pop(sender, None)
msg.body("β Cancelled.")
return str(resp)
try:
# --- Parse with Claude ---
parse_response = claude.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": incoming_msg}]
)
raw = parse_response.content[0].text.replace('```json', '').replace('```', '').strip()
parsed = json.loads(raw)
# --- Execute ---
if parsed['action'] == 'read':
answer = read_sheet(parsed['query'])
msg.body(answer)
elif parsed['action'] == 'add':
row = parsed['row']
pending_confirmations[sender] = row
preview = (
f"π *Add this row?*\n\n"
f"Subject: {row['subject']}\n"
f"Type: {row.get('type', 'Assignment')}\n"
f"Title: {row.get('title', '')}\n"
f"Date: {row.get('due_date', '')}\n"
f"Time: {row.get('time', '')}\n"
f"Notes: {row.get('notes', '-')}\n\n"
f"Reply *YES* to confirm, *NO* to cancel."
)
msg.body(preview)
elif parsed['action'] == 'update_status':
result = update_status(parsed['row_num'], parsed['new_status'])
msg.body(result)
elif parsed['action'] == 'help':
msg.body(HELP_MESSAGE)
elif parsed['action'] == 'unknown':
msg.body(f"π€ {parsed['message']}\n\nSend /help for examples.")
except Exception as e:
msg.body(f"β Error: {str(e)[:200]}\n\nSend /help")
return str(resp)
@app.route('/', methods=['GET'])
def health():
return 'OK'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
5.2 - requirements.txt
Flask==3.0.0
anthropic==0.34.0
twilio==8.10.0
google-api-python-client==2.100.0
google-auth==2.23.0
gunicorn==21.2.0
5.3 - Dockerfile
Same as Option 2.
6Deploy to Cloud Run
# Store service account in Secret Manager
gcloud secrets create sheets-service-account --data-file=service-account.json
# Deploy
gcloud run deploy tracker-whatsapp-bot \
--source . \
--region asia-southeast1 \
--allow-unauthenticated \
--set-env-vars TWILIO_ACCOUNT_SID=AC...,TWILIO_AUTH_TOKEN=xxx,TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886,ANTHROPIC_KEY=sk-ant-xxx,SHEET_ID=your_sheet_id,ALLOWED_NUMBERS=whatsapp:+919876543210 \
--set-secrets /secrets/service-account.json=sheets-service-account:latest
Note the deployed URL.
7Configure Twilio Webhook
- Twilio Console β Messaging β Try WhatsApp β Sandbox settings
- In "When a message comes in":
- URL:
https://tracker-whatsapp-bot-xxxxx.a.run.app/whatsapp- Method: HTTP POST - Save
8Users opt-in
For each user (including you):
- Save Twilio Sandbox number (
+1 415 523 8886) in contacts - Send WhatsApp message:
join yellow-tiger(your specific code from Twilio) - Twilio confirms opt-in
- Their number is now valid (until 72hr inactivity)
Add each user's number to Cloud Run env var:
gcloud run services update tracker-whatsapp-bot \
--region asia-southeast1 \
--update-env-vars ALLOWED_NUMBERS=whatsapp:+919876543210,whatsapp:+911234567890
9Test
Message the Twilio bot from your WhatsApp:
/help β shows help menu
what's due? β returns sheet items
add GPU quiz on Oct 15 6pm β asks for confirmation
yes β adds the row
Step 10 (Optional): Production WhatsApp Business API
Sandbox has limits: - Users must opt-in with keyword every 72hr - 24-hour conversation window - Twilio-branded messages
For real batch deployment (50+ users), you need WhatsApp Business API approval:
- Register a Meta Business Account
- Verify your business (may require GST/PAN in India)
- Get a WhatsApp Business number
- Cost: ~$50-100 setup + per-message pricing
When you should upgrade: - Regular use by 20+ people - Need branded sender name - Users complain about re-opting-in
For a class batch, Sandbox works fine - just remind everyone to re-send join keyword if it stops.
Cost breakdown
At 500 messages/month (small batch usage):
| Service | Cost |
|---|---|
| Cloud Run | Free tier |
| Twilio WhatsApp Sandbox | Free (test) |
| Twilio WhatsApp Business | ~$0.005/message β $2.50 |
| Claude API | ~$3-5 |
| Total (sandbox) | ~$3-5/month |
| Total (business) | ~$5-15/month |
Security best practices
Same as Option 2, plus: - Twilio webhook signature validation (verify requests actually come from Twilio):
from twilio.request_validator import RequestValidator
validator = RequestValidator(TWILIO_AUTH_TOKEN)
@app.route('/whatsapp', methods=['POST'])
def whatsapp():
# Validate request came from Twilio
twilio_signature = request.headers.get('X-Twilio-Signature', '')
if not validator.validate(request.url, request.form, twilio_signature):
return 'Unauthorized', 403
# ... rest of handler
Advanced features
Broadcast morning digest to all users
Cloud Scheduler β 8 AM daily β hits /daily-digest endpoint:
@app.route('/daily-digest', methods=['POST'])
def daily_digest():
digest = read_sheet("What's due today and tomorrow?")
for number in ALLOWED_NUMBERS:
twilio_client.messages.create(
from_=TWILIO_WHATSAPP_NUMBER,
to=number,
body=f"π
*Morning Digest*\n\n{digest}"
)
return 'ok'
Voice notes β transcribe
Twilio doesn't natively transcribe. Options: - Whisper API (OpenAI) - $0.006/min - Deepgram - $0.0043/min
if request.values.get('MediaContentType0', '').startswith('audio'):
audio_url = request.values.get('MediaUrl0')
transcript = transcribe_via_whisper(audio_url)
# Then process transcript as text
Sharing with your batch - announcement template
Hi all π
I've set up a WhatsApp bot for our TS-03 tracker.
You can text it naturally like:
π± Save this number: +1 415 523 8886
1οΈβ£ WhatsApp message: join <your-code-here>
2οΈβ£ Then just ask: "what's due this week?" or "show GPU items"
Bot updates our shared Google Sheet automatically π
Try it and ping me if you get stuck!
β [Your Name]
Summary
Batch-scale access via a channel everyone already has. Highest impact for group adoption in India. Setup effort is real but pays off across many users.
Combine with: - Option 1 or 3 for your personal iPhone-first flow - Option 5 for voice-first bells and whistles
Questions or corrections: [email protected]