When we started building Insell — a WhatsApp API + CRM platform for Indian SMBs — we ran into every integration challenge you'd expect: webhook setup, message templates, session handling, and dealing with Meta's approval process. This post covers exactly how we built it and what we learned.
Why WhatsApp API for Indian SMBs?
India has over 500 million WhatsApp users. For small businesses — coaching institutes, e-commerce shops, real estate agents, clinics — WhatsApp is not just a messaging app. It's where their customers actually are and where they expect to be contacted.
Email open rates in India hover around 15-20%. WhatsApp messages get 90%+ open rates within 3 minutes. The business case writes itself.
Setting Up the WhatsApp Business API
Meta provides the WhatsApp Business API through approved Business Solution Providers (BSPs). Here's the flow we implemented:
1. Meta App Setup
# You need a Meta Developer account and a verified Business Manager
# Create an app at developers.facebook.com
# Add WhatsApp product to your app
2. Phone Number Registration
Every WhatsApp Business account needs a dedicated phone number. The number cannot be actively used on regular WhatsApp.
import requests
def register_phone_number(access_token, phone_number_id):
url = f"https://graph.facebook.com/v18.0/{phone_number_id}/register"
headers = {"Authorization": f"Bearer {access_token}"}
payload = {
"messaging_product": "whatsapp",
"pin": "YOUR_6_DIGIT_PIN"
}
response = requests.post(url, headers=headers, json=payload)
return response.json()
3. Webhook Configuration
Webhooks are the backbone of the integration. Meta sends all incoming messages, delivery receipts, and read receipts to your webhook endpoint.
from flask import Flask, request, jsonify
import hashlib
import hmac
app = Flask(__name__)
@app.route('/webhook', methods=['GET'])
def verify_webhook():
"""Meta sends a GET request to verify webhook ownership"""
verify_token = request.args.get('hub.verify_token')
challenge = request.args.get('hub.challenge')
if verify_token == 'YOUR_VERIFY_TOKEN':
return challenge
return 'Verification failed', 403
@app.route('/webhook', methods=['POST'])
def receive_message():
"""Handle incoming WhatsApp messages"""
data = request.json
if data.get('object') == 'whatsapp_business_account':
for entry in data.get('entry', []):
for change in entry.get('changes', []):
value = change.get('value', {})
messages = value.get('messages', [])
for message in messages:
handle_message(message)
return jsonify({'status': 'ok'})
def handle_message(message):
message_type = message.get('type')
from_number = message.get('from')
if message_type == 'text':
text = message.get('text', {}).get('body', '')
# Process and respond
send_reply(from_number, f"Received: {text}")
elif message_type == 'interactive':
# Handle button clicks, list selections
handle_interactive(message)
4. Sending Messages
WhatsApp API distinguishes between two types of messages:
Template Messages (for business-initiated conversations):
def send_template_message(to_number, template_name, language_code='en_US', params=None):
url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"
headers = {
"Authorization": f"Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"messaging_product": "whatsapp",
"to": to_number,
"type": "template",
"template": {
"name": template_name,
"language": {"code": language_code},
"components": []
}
}
if params:
payload["template"]["components"].append({
"type": "body",
"parameters": [{"type": "text", "text": p} for p in params]
})
response = requests.post(url, headers=headers, json=payload)
return response.json()
# Example: Send OTP template
send_template_message(
to_number="+919876543210",
template_name="otp_verification",
params=["482916"]
)
Session Messages (replying within 24-hour window):
def send_text_reply(to_number, message_text):
url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"
payload = {
"messaging_product": "whatsapp",
"to": to_number,
"type": "text",
"text": {"body": message_text}
}
# ... same headers as above
response = requests.post(url, headers=headers, json=payload)
return response.json()
The CRM Layer: Linking Conversations to Contacts
The raw API gives you messages, but building a CRM requires linking conversations to contact records. Here's our data model:
# models.py (Django ORM)
from django.db import models
class Contact(models.Model):
phone_number = models.CharField(max_length=20, unique=True)
name = models.CharField(max_length=255, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
tags = models.JSONField(default=list)
custom_fields = models.JSONField(default=dict)
class Conversation(models.Model):
contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
status = models.CharField(
max_length=20,
choices=[('open', 'Open'), ('resolved', 'Resolved'), ('pending', 'Pending')],
default='open'
)
assigned_to = models.ForeignKey('auth.User', null=True, blank=True, on_delete=models.SET_NULL)
last_message_at = models.DateTimeField(null=True)
class Message(models.Model):
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE)
wa_message_id = models.CharField(max_length=100, unique=True)
direction = models.CharField(choices=[('inbound', 'Inbound'), ('outbound', 'Outbound')], max_length=10)
message_type = models.CharField(max_length=20) # text, image, audio, etc.
content = models.JSONField()
timestamp = models.DateTimeField()
status = models.CharField(max_length=20, default='sent') # sent, delivered, read, failed
Handling the 24-Hour Session Window
This is where most developers trip up. Meta only allows free-form messages within 24 hours of the last customer message. After that, you must use approved templates.
from datetime import datetime, timedelta
import pytz
def can_send_session_message(conversation):
"""Check if we're within the 24-hour session window"""
if not conversation.last_inbound_at:
return False
now = datetime.now(pytz.UTC)
window_end = conversation.last_inbound_at + timedelta(hours=24)
return now < window_end
def send_smart_message(conversation, message_text, template_name=None):
"""Send session or template message based on window"""
if can_send_session_message(conversation):
return send_text_reply(conversation.contact.phone_number, message_text)
elif template_name:
return send_template_message(
conversation.contact.phone_number,
template_name
)
else:
raise Exception("Outside 24h window and no template provided")
Broadcast Campaigns for Lead Nurturing
One of the highest-value features for Indian SMBs is broadcast messaging — sending approved templates to opted-in contacts for follow-ups, reminders, and offers.
import time
def send_broadcast(contact_list, template_name, params_func, rate_limit=80):
"""
Send template broadcasts with rate limiting
Meta allows ~80 messages/second on standard tier
"""
results = {'sent': 0, 'failed': 0, 'errors': []}
for i, contact in enumerate(contact_list):
try:
params = params_func(contact) # Personalize per contact
result = send_template_message(
contact.phone_number,
template_name,
params=params
)
if 'messages' in result:
results['sent'] += 1
else:
results['failed'] += 1
results['errors'].append({
'contact': contact.phone_number,
'error': result.get('error', {}).get('message', 'Unknown')
})
except Exception as e:
results['failed'] += 1
# Rate limiting
if (i + 1) % rate_limit == 0:
time.sleep(1)
return results
Key Lessons After Building This
Phone number quality rating matters — Meta assigns quality ratings to your number based on user blocks and complaints. Keep templates relevant and honour opt-outs immediately.
Template approval takes 1-48 hours — Build your template library before you need it urgently.
Webhook reliability is critical — Use a queue (Celery + Redis works well) to process webhooks asynchronously. Don't process in the webhook handler itself.
Indian phone numbers need country code — Always store and send numbers with +91 prefix.
Test with actual Indian SIM cards — Some message types render differently on older Android versions common in India.
What We Shipped
We turned all of this into Insell — a ready-to-use WhatsApp CRM built specifically for Indian SMBs. If you're an agency or developer looking to offer WhatsApp automation to your clients without building from scratch, check it out.
The platform handles the API complexity (webhooks, session management, template approvals, broadcasts) so your clients can focus on conversations, not infrastructure.
Happy to answer any questions about the WhatsApp Business API integration in the comments. This stuff has a lot of edge cases that aren't documented well.
Tags: whatsapp, python, api, crm, india
Top comments (0)