Skip to main content
The Sonore Phone Agent supports multi-tenancy, allowing different organizations or departments to use separate configurations, prompts, and phone numbers.

Architecture Overview

The tenant system consists of three main components:
  1. Phone-Tenant Mapping - Maps incoming phone numbers to tenant IDs
  2. Tenant Configuration - Stores tenant-specific settings and features
  3. Tenant Prompts - Manages versioned prompts per tenant

Phone Number to Tenant Mapping

Database Collection: phone-tenant-map

This collection maps incoming phone numbers to tenant identifiers.

Schema

{
  "_id": ObjectId,
  "phone_number": "+33676530019",
  "client_id": "axeoguyane",
  "tenant_id": "tenant-test-001"
}
phone_number
string
required
The incoming phone number in E.164 format (e.g., +33676530019)This is the number that callers dial to reach this tenant’s agent.
tenant_id
string
required
Unique identifier for the tenant. Used to look up configuration and prompts.Format: tenant-{environment}-{number} (e.g., tenant-prod-001)
client_id
string
required
Stable business identifier for the client organizationExample: axeoguyane, axeoservices

Creating a Phone Mapping

db['phone-tenant-map'].insertOne({
  phone_number: "+33123456789",
  client_id: "mycompany",
  tenant_id: "tenant-prod-001"
})

Tenant Resolution Process

When a call arrives:
  1. Extract the phone_number from the incoming call
  2. Query phone-tenant-map collection for matching record
  3. Retrieve the tenant_id from the result
  4. Load tenant configuration using the tenant_id
Implementation: src/apps/calls/app/tenant_resolution.py:18
from src.apps.calls.app.tenant_resolution import TenantResolver

resolver = TenantResolver(
    client=mongo_client,
    db="sonore-phone-agent",
    collection="phone-tenant-map"
)

tenant_id = await resolver.resolve_tenant("+33676530019")
# Returns: "tenant-test-001"

Tenant Configuration

Database Collection: tenant_config

This collection stores detailed configuration for each tenant.

MongoDB Schema

{
  "$jsonSchema": {
    "bsonType": "object",
    "required": [
      "_id",
      "tenant_id",
      "client_id",
      "display_name",
      "summary_to",
      "timezone",
      "status",
      "schema_version",
      "created_at",
      "updated_at"
    ],
    "properties": {
      "_id": { 
        "bsonType": "string", 
        "description": "Use tenant_id as _id" 
      },
      "tenant_id": { "bsonType": "string" },
      "client_id": { 
        "bsonType": "string", 
        "description": "Stable identifier used for display/business naming" 
      },
      "display_name": { "bsonType": "string" },
      "summary_to": {
        "bsonType": "array",
        "items": { "bsonType": "string" },
        "description": "Recipients for post-call summaries"
      },
      "reply_to": { "bsonType": ["string", "null"] },
      "timezone": { 
        "bsonType": "string", 
        "description": "IANA timezone, e.g. Europe/Paris" 
      },
      "locale": { 
        "bsonType": ["string", "null"], 
        "description": "Optional, e.g. fr-FR" 
      },
      "status": { "enum": ["active", "disabled"] },
      "features": {
        "bsonType": "object",
        "description": "Feature flags (coarse toggles). Keep empty for v0."
      },
      "metadata": {
        "bsonType": "object",
        "description": "Reserved for future non-secret config (routing/tool policy references)."
      },
      "schema_version": { "bsonType": "int" },
      "created_at": { "bsonType": "date" },
      "updated_at": { "bsonType": "date" }
    },
    "additionalProperties": true
  }
}

Configuration Fields

_id
string
required
Primary key - should match tenant_id
tenant_id
string
required
Unique tenant identifier
client_id
string
required
Stable business identifier for display and naming purposes
display_name
string
required
Human-readable name for the tenant (e.g., “AXEO Services Guyane”)
summary_to
string[]
required
Email addresses to receive post-call summariesExample: ["[email protected]", "[email protected]"]
reply_to
string | null
Optional reply-to email address for summary emails
timezone
string
required
IANA timezone for the tenant’s localeExamples: Europe/Paris, America/New_York, Asia/Tokyo
locale
string | null
Optional locale code for language/region settingsExamples: fr-FR, en-US, es-ES
status
string
required
Tenant activation statusOptions:
  • active - Tenant can receive calls
  • disabled - Tenant is deactivated
features
object
Feature configuration object. See Feature Configuration below.
metadata
object
Reserved for future use. Can store routing policies, tool configurations, etc.
schema_version
integer
required
Configuration schema version for migrations
created_at
date
required
Tenant creation timestamp
updated_at
date
required
Last configuration update timestamp

Example Tenant Configuration

{
  "_id": "tenant-prod-001",
  "tenant_id": "tenant-prod-001",
  "client_id": "axeoservices",
  "display_name": "AXEO Services - La Baule",
  "summary_to": ["[email protected]", "[email protected]"],
  "reply_to": "[email protected]",
  "timezone": "Europe/Paris",
  "locale": "fr-FR",
  "status": "active",
  "features": {
    "refer": {
      "enabled": true,
      "require_confirmation": false,
      "destinations": [
        {
          "destination_id": "commercial",
          "label": "Mme Gogny",
          "target_uri": "tel:+33240000001",
          "description_for_model": "For sales, quotes, and new client inquiries",
          "enabled": true,
          "priority": 1
        },
        {
          "destination_id": "planning",
          "label": "le service planning",
          "target_uri": "tel:+33240000002",
          "description_for_model": "For scheduling and intervention management",
          "enabled": true,
          "priority": 2
        }
      ]
    }
  },
  "metadata": {},
  "schema_version": 1,
  "created_at": ISODate("2026-01-15T10:00:00Z"),
  "updated_at": ISODate("2026-03-02T14:30:00Z")
}

Feature Configuration

The features object controls optional functionality for each tenant.

Refer Feature (Call Transfer)

The refer feature enables call transfer to specific departments or phone numbers. Model: src/models/instructions/features/refer.py:52
features.refer.enabled
boolean
default:"false"
Enable or disable call transfer functionality
features.refer.require_confirmation
boolean
default:"true"
Whether to ask caller for confirmation before transferring
features.refer.handoff_phrase
string | null
Custom phrase to say when transferring (overrides default)
features.refer.destinations
array
List of available transfer destinations

Destination Configuration

destination_id
string
required
Unique identifier for the destination (lowercase, alphanumeric with underscores)Pattern: ^[a-z][a-z0-9_]{1,40}$Examples: commercial, planning, comptabilite
label
string
required
Human-readable label used in conversationExamples: "Mme Gogny", "le service planning", "la comptabilité"
target_uri
string
required
Transfer target in URI formatFormats:
description_for_model
string
required
Description that helps the AI decide when to use this destinationExample: "For sales inquiries, quotes, and new customer requests"
enabled
boolean
default:"true"
Whether this destination is currently available
priority
integer
default:"0"
Priority ordering for display/selection (lower = higher priority)
business_hours
object | null
Optional business hours restriction for this destination
business_hours.tz
string
default:"Europe/Paris"
Timezone for business hours (IANA format)
business_hours.rrule
string
required
Recurrence rule (RFC 5545 RRULE format) defining when destination is availableExample: "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0"
business_hours.duration_minutes
integer
required
Duration in minutes that the destination is availableRange: 1 to 1440 (24 hours)
after_hours_destination_id
string | null
Fallback destination ID to use when outside business hoursMust reference another valid destination_id in the same tenant’s destinations list.

Example: Business Hours Configuration

{
  "destination_id": "sales",
  "label": "l'équipe commerciale",
  "target_uri": "tel:+33140000000",
  "description_for_model": "For sales and quotes",
  "enabled": true,
  "priority": 1,
  "business_hours": {
    "tz": "Europe/Paris",
    "rrule": "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR",
    "duration_minutes": 480
  },
  "after_hours_destination_id": "voicemail"
}

Python Models

The tenant configuration is validated using Pydantic models:

TenantConfig Model

File: src/models/instructions/tenant_config.py:14
from src.models.instructions.tenant_config import TenantConfig

# Load from MongoDB document
config = TenantConfig.from_tenant_doc(doc)

# Access properties
print(config.tenant_id)  # "tenant-prod-001"
print(config.timezone)   # "Europe/Paris"
print(config.locale)     # "fr-FR"

# Access features
if config.features.refer.enabled:
    destinations = config.features.refer.destinations

Fields

tenant_id
string
required
Tenant identifier
timezone
string
default:"Europe/Paris"
IANA timezone
locale
string | null
Locale code
tools_config_version
integer
default:"1"
Version of the tools configuration
features
FeaturesConfig
Feature configuration object

Creating a New Tenant

To set up a new tenant:

Step 1: Create Phone Mapping

db['phone-tenant-map'].insertOne({
  phone_number: "+33987654321",
  client_id: "newclient",
  tenant_id: "tenant-prod-002"
})

Step 2: Create Tenant Configuration

db['tenant_config'].insertOne({
  _id: "tenant-prod-002",
  tenant_id: "tenant-prod-002",
  client_id: "newclient",
  display_name: "New Client Inc.",
  summary_to: ["[email protected]"],
  reply_to: null,
  timezone: "Europe/Paris",
  locale: "fr-FR",
  status: "active",
  features: {
    refer: {
      enabled: false,
      require_confirmation: true,
      destinations: []
    }
  },
  metadata: {},
  schema_version: 1,
  created_at: new Date(),
  updated_at: new Date()
})

Step 3: Create Tenant Prompts

See Prompts Configuration for details on setting up tenant-specific prompts.
After creating a new tenant, test the phone mapping by making a test call to the configured phone number.

Build docs developers (and LLMs) love