Skip to content
Learn Agentic AI13 min read0 views

Agent White-Labeling: Building Customizable Agents for Reseller Partners

Architect a white-label AI agent system that lets reseller partners rebrand, customize behavior, and deploy agents under their own identity. Covers multi-tenant isolation, branding configuration, and partner management APIs.

What White-Labeling Means for Agents

White-labeling lets a partner take your AI agent, apply their branding, customize its behavior for their market, and present it to their customers as their own product. The end user never knows a third party built the underlying agent.

This model accelerates distribution. Instead of selling to thousands of end customers directly, you sell to fifty partners who each serve hundreds of customers. But the architecture must support deep customization without forking the codebase — every partner runs the same agent engine with different configurations.

The Branding Configuration Layer

Every customer-facing aspect of the agent must be configurable per partner. A branding configuration captures identity, tone, and visual presentation:

from dataclasses import dataclass, field
from typing import Optional


@dataclass
class BrandingConfig:
    partner_id: str
    company_name: str
    agent_name: str = "Assistant"
    agent_persona: str = ""
    greeting_message: str = "Hello! How can I help you today?"
    farewell_message: str = "Thanks for chatting. Have a great day!"
    primary_color: str = "#0066CC"
    logo_url: str = ""
    support_email: str = ""
    custom_css: str = ""
    language: str = "en"
    tone: str = "professional"  # professional, casual, formal
    forbidden_topics: list[str] = field(default_factory=list)
    custom_instructions: str = ""


@dataclass
class PartnerConfig:
    partner_id: str
    branding: BrandingConfig
    enabled_features: list[str] = field(default_factory=list)
    max_conversations_per_month: int = 10000
    allowed_models: list[str] = field(
        default_factory=lambda: ["gpt-4o-mini"]
    )
    custom_tools: list[str] = field(default_factory=list)
    webhook_url: Optional[str] = None
    api_key: str = ""

Dynamic Prompt Injection

The agent's system prompt must incorporate partner branding at runtime. This is not simple string concatenation — it requires a template system that layers base instructions with partner-specific customization:

class WhiteLabelPromptBuilder:
    BASE_TEMPLATE = (
        "You are {agent_name}, an AI assistant for "
        "{company_name}.\n\n"
        "TONE: Communicate in a {tone} manner.\n\n"
        "{custom_instructions}\n\n"
        "RESTRICTIONS:\n"
        "- Never mention that you are built by a third party\n"
        "- Never reference other companies or competitors\n"
        "{forbidden_topics_block}"
        "- Always identify yourself as {agent_name} from "
        "{company_name}\n"
    )

    def build_system_prompt(
        self, partner_config: PartnerConfig
    ) -> str:
        branding = partner_config.branding

        forbidden_block = ""
        if branding.forbidden_topics:
            lines = [
                f"- Never discuss: {topic}"
                for topic in branding.forbidden_topics
            ]
            forbidden_block = "\n".join(lines) + "\n"

        return self.BASE_TEMPLATE.format(
            agent_name=branding.agent_name,
            company_name=branding.company_name,
            tone=branding.tone,
            custom_instructions=(
                branding.custom_instructions
                or "Help users with their questions accurately."
            ),
            forbidden_topics_block=forbidden_block,
        )

The prompt builder ensures the agent always identifies as the partner's product, never reveals the underlying platform, and respects partner-specific content restrictions.

Multi-Tenant Request Routing

Every incoming request must be routed to the correct partner configuration. A middleware layer resolves the partner from the request context and injects the appropriate configuration:

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

from starlette.middleware.base import (
    BaseHTTPMiddleware,
)
from starlette.requests import Request


class PartnerResolutionMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, partner_store):
        super().__init__(app)
        self.partner_store = partner_store

    async def dispatch(self, request: Request, call_next):
        partner_id = self._resolve_partner(request)
        if not partner_id:
            return JSONResponse(
                {"error": "Invalid partner credentials"},
                status_code=401,
            )

        partner_config = await self.partner_store.get(partner_id)
        if not partner_config:
            return JSONResponse(
                {"error": "Partner not found"},
                status_code=404,
            )

        # Inject config into request state
        request.state.partner_config = partner_config
        response = await call_next(request)
        return response

    def _resolve_partner(self, request: Request) -> str | None:
        # Check API key header
        api_key = request.headers.get("X-Partner-Key")
        if api_key:
            return self.partner_store.resolve_by_key(api_key)

        # Check subdomain
        host = request.headers.get("host", "")
        subdomain = host.split(".")[0]
        return self.partner_store.resolve_by_subdomain(subdomain)

Partners are resolved either by API key (for programmatic access) or by subdomain (for hosted widget deployments). The configuration flows through the entire request lifecycle.

Partner Management API

Partners need self-service tools to manage their branding and monitor usage:

from fastapi import APIRouter, Depends

router = APIRouter(prefix="/api/partners")


@router.put("/{partner_id}/branding")
async def update_branding(
    partner_id: str,
    branding_update: dict,
    partner_store=Depends(get_partner_store),
):
    config = await partner_store.get(partner_id)
    if not config:
        raise HTTPException(status_code=404)

    for key, value in branding_update.items():
        if hasattr(config.branding, key):
            setattr(config.branding, key, value)

    config.branding.updated_at = datetime.utcnow()
    await partner_store.save(config)
    return {"status": "updated", "partner_id": partner_id}


@router.get("/{partner_id}/usage")
async def get_usage(
    partner_id: str,
    period: str = "current_month",
    usage_service=Depends(get_usage_service),
):
    usage = await usage_service.get_partner_usage(
        partner_id, period
    )
    return {
        "partner_id": partner_id,
        "period": period,
        "conversations": usage["conversations"],
        "messages": usage["messages"],
        "limit": usage["limit"],
        "utilization_pct": round(
            usage["conversations"] / usage["limit"] * 100, 1
        ),
    }

Data Isolation

Each partner's conversation data must be strictly isolated. Use tenant-scoped database queries to prevent cross-partner data leakage:

class TenantScopedConversationStore:
    def __init__(self, db_pool):
        self.db = db_pool

    async def get_conversations(
        self, partner_id: str, limit: int = 50
    ) -> list[dict]:
        query = """
            SELECT id, user_id, started_at, message_count
            FROM conversations
            WHERE partner_id = $1
            ORDER BY started_at DESC
            LIMIT $2
        """
        return await self.db.fetch(query, partner_id, limit)

    async def create_conversation(
        self, partner_id: str, user_id: str
    ) -> str:
        query = """
            INSERT INTO conversations (partner_id, user_id)
            VALUES ($1, $2)
            RETURNING id
        """
        row = await self.db.fetchrow(
            query, partner_id, user_id
        )
        return row["id"]

Every query includes the partner_id filter. There is no code path that can retrieve another partner's data.

FAQ

How do you prevent partners from seeing the underlying platform brand?

The system prompt explicitly instructs the agent never to mention the platform provider. Combine this with output guardrails that scan responses for platform brand names and block them. Also ensure error messages, API responses, and widget UI never reference the platform.

How do you handle feature differences between partner tiers?

Store an enabled_features list in the partner configuration. Check feature flags before executing capabilities. Premium partners might get access to advanced models, analytics dashboards, or custom tool integrations. The same codebase serves all tiers — features are toggled by configuration.

What happens when you update the base agent logic?

All partners receive the update simultaneously since they share the same engine. Use feature flags and gradual rollouts to minimize risk. Partner-specific customizations live in configuration, not code, so base updates do not overwrite partner settings.


#WhiteLabel #MultiTenant #AgentCustomization #PartnerManagement #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.