Zappio Team
AI & Real Estate Experts · 6 July 2026 · 14 min read
Zappio Team
AI & Real Estate Experts · 6 July 2026 · 14 min read
A real estate developer group operating 10 active projects simultaneously — three in Gurgaon, two in Noida, two in Pune, one in Hyderabad, one in Mumbai, and one pre-launch in Bangalore — does not have 10 separate AI Calling deployments. It has one AI Calling infrastructure serving 10 tenants, each with its own project knowledge base, calling script, qualification criteria, CRM configuration, telephony number pool, and performance dashboard.
The distinction between a multi-project deployment and a multi-tenant architecture is not semantic. In a multi-project deployment, 10 separate instances of the AI system run independently, and operational overhead grows linearly with project count. A multi-tenant architecture is a single, shared infrastructure that logically isolates each project's configuration, data, and performance metrics — while sharing the underlying compute, telephony, and LLM inference layers that make shared deployment cost-effective.
The first design decision in multi-tenant AI Calling is which components are shared across tenants and which are logically isolated per tenant.
Every tenant in the system is represented by a TenantConfig object that defines all project-specific parameters:
@dataclass
class TenantConfig:
# Identity
tenant_id: str # "PROJ-GGM-001"
tenant_name: str # "Sector 65 Golf Course Extension"
developer_group_id: str # "DEVGROUP-ACME"
# Project knowledge base
project_name: str
project_location: str
rera_number: str
harera_number: Optional[str]
# Pricing and inventory
price_floor_inr: int
price_ceiling_inr: int
available_bhk_configs: list # [3, 4]
carpet_area_by_bhk: dict # {3: 1050, 4: 1450}
sba_by_bhk: dict # {3: 1680, 4: 2320}
possession_date: str
possession_committed_rera: bool
# Calling configuration
calling_script_template: str
qualification_objectives: list
disqualification_rules: dict
objection_handling_map: dict
site_visit_slot_options: list
# Language and voice
primary_language: str # "hinglish"
tts_voice_config: dict
# Telephony
number_pool: list
telephony_provider: str # "exotel"
telephony_api_credentials: str # Reference to secrets manager
# CRM integration
crm_provider: str # "sell.do"
crm_campaign_id: str
crm_webhook_url: str
crm_field_mapping: dict
# Human fallback
escalation_queue: str
fallback_agent_extensions: list
# Performance thresholds
max_call_duration_seconds: int
transfer_rate_alert_threshold: floatTenant configs are stored with version history — every change creates a new version record, enabling rollback if a script change degrades conversion rate. The config is loaded into a Redis cache on startup and refreshed on update events.
Every inbound webhook from the telephony layer or outbound call event carries a phone number or campaign ID that the router uses to identify the tenant:
class MultiTenantCallRouter:
def __init__(self, config_store: TenantConfigStore, redis: Redis):
self.config_store = config_store
self.redis = redis
async def route_incoming_call(self, call_event: dict) -> tuple[TenantConfig, CallSession]:
tenant_id = await self._identify_tenant(call_event)
if not tenant_id:
await self._handle_unknown_caller(call_event)
return None, None
tenant_config = await self.config_store.get(tenant_id)
lead_data = await self._load_lead_data(
tenant_id=tenant_id,
phone_number=call_event.get("from_number")
)
session = CallSession(
call_id=call_event["call_id"],
tenant_id=tenant_id,
tenant_config=tenant_config,
lead_data=lead_data,
call_start_time=datetime.utcnow()
)
await self.redis.setex(
f"session:{tenant_id}:{session.call_id}",
ttl=7200,
value=session.to_json()
)
return tenant_config, session
async def _identify_tenant(self, call_event: dict) -> Optional[str]:
# Method 1: Match outbound caller ID to tenant number pool
from_number = call_event.get("to_number")
tenant = await self.config_store.find_by_number(from_number)
if tenant:
return tenant.tenant_id
# Method 2: Campaign ID in event metadata
campaign_id = call_event.get("campaign_id")
if campaign_id:
return await self.config_store.find_by_campaign(campaign_id)
return NoneThe LLM system prompt is assembled dynamically from the tenant config on every inference call — there is no static system prompt in a multi-tenant system:
def assemble_system_prompt(
tenant_config: TenantConfig,
session: CallSession,
state_context: str
) -> str:
prompt = f"""ROLE:
You are a professional outbound qualification specialist for {tenant_config.developer_group_id},
calling about {tenant_config.project_name} in {tenant_config.project_location}.
Your name is Priya.
YOUR OBJECTIVES (in priority order):
{chr(10).join(f"{i+1}. {obj}" for i, obj in enumerate(tenant_config.qualification_objectives))}
PROJECT FACTS (use ONLY these — never invent):
- Project: {tenant_config.project_name}
- Location: {tenant_config.project_location}
- RERA: {tenant_config.rera_number or tenant_config.harera_number}
- Available: {_format_pricing(tenant_config)}
- Possession: {tenant_config.possession_date} (RERA committed: {tenant_config.possession_committed_rera})
- Site visit slots: {", ".join(tenant_config.site_visit_slot_options)}
DISQUALIFICATION RULES:
- Budget below floor after 2 attempts → trigger transfer
- BHK not in {tenant_config.available_bhk_configs} → attempt alternate or transfer
LANGUAGE: {tenant_config.primary_language.upper()} — mirror buyer's language mix exactly.
RESPONSE RULES:
- Maximum 35 words per response
- One question per turn only
- If question outside PROJECT FACTS → offer WhatsApp + site visit
{state_context}
"""
return promptData isolation is enforced at four layers.
With 10 projects running simultaneously, a single performance degradation may affect all tenants or only a subset. The monitoring system must distinguish tenant-specific problems from system-wide problems.
| Metric | System-Wide Alert | Tenant-Specific Alert |
|---|---|---|
| LLM inference P95 latency | > 800ms across all tenants | > 800ms for single tenant only |
| ASR WER spike | > 15% across all active calls | > 15% for calls in specific city/market |
| Transfer rate | > 20% system-wide | > 20% for single project |
| Call drop rate | > 5% system-wide | > 5% for specific telephony number pool |
| CRM sync failure | > 1% system-wide | > 1% for specific CRM integration |
| Concurrent call ceiling | > 80% of provisioned capacity | Single tenant consuming > 40% of pool |
Each tenant is assigned a maximum concurrent call quota — typically proportional to monthly lead volume — so a tenant running a weekend campaign launch cannot starve the infrastructure of other tenants:
TENANT_CONCURRENCY_LIMITS = {
"PROJ-GGM-001": 50, # Large project, high volume
"PROJ-GGM-002": 30, # Medium project
"PROJ-NOI-001": 40,
"PROJ-PUN-001": 25,
# ... etc.
}
async def can_initiate_call(tenant_id: str) -> bool:
current = await redis.get(f"concurrency:{tenant_id}")
limit = TENANT_CONCURRENCY_LIMITS.get(tenant_id, 20)
return int(current or 0) < limitA multi-tenant system requires two tiers of operational controls. Group-level controls — managed by the developer group's technical or marketing head — include pausing all calling system-wide, viewing the combined performance dashboard, adjusting the global concurrent call ceiling, and deploying system-wide prompt updates for language or compliance rules. Project-level controls — managed by individual project marketing managers — include pausing or resuming calling for their specific project only, updating site visit slot availability, adjusting calling hours, reviewing their own performance dashboard, and exporting call transcripts for quality review.
This two-tier control model prevents project managers from accidentally affecting other projects while giving them full autonomy over their own deployment.
Migrating 10 projects from human calling to multi-tenant AI Calling is a sequenced deployment, not a simultaneous switch.
Disclaimer: Multi-tenant architecture patterns, concurrency limits, data isolation approaches, and operational controls described in this article reflect production deployment practices as of Q2 2026. Actual infrastructure requirements, scaling thresholds, and resource quotas depend on call volume, LLM inference latency characteristics, and telephony provider capacity. Code samples are illustrative and require adaptation to specific technology stacks, cloud providers, and organizational deployment requirements. Data isolation designs should be reviewed by a qualified security engineer before production deployment.