Chapter 325m

CRM integration & caller context

CRM Integration: Caller Context and Conversation Logging

The best agents — human or AI — know who is calling before they pick up. CRM integration makes this possible. When a call arrives, the system looks up the caller's phone number, pulls their history, and injects it into the agent's instructions. When the call ends, the conversation summary is logged back. This chapter connects your contact center to Salesforce and HubSpot.

CRM lookupCaller enrichmentConversation loggingSalesforceHubSpot

What you'll learn

  • How to look up caller information from a CRM on incoming calls
  • Building a screen pop that injects caller context into agent instructions
  • Why AI screen pop is more powerful than traditional human agent screen pop
  • Logging call outcomes and conversation summaries back to the CRM
  • Integration patterns for Salesforce and HubSpot APIs

Caller lookup on incoming calls

When a call arrives through your SIP trunk, the caller ID (ANI) is the key. Use it to search the CRM before the agent even greets the caller. This lookup must be fast — under 2 seconds — and must fail gracefully.

crm_lookup.pypython
from dataclasses import dataclass
import httpx


@dataclass
class CallerProfile:
  name: str
  company: str
  email: str
  phone: str
  account_id: str
  lifetime_value: float
  open_tickets: int
  last_contact: str
  notes: str
  priority: str  # "vip", "standard", "new"


class CRMLookup:
  def __init__(self, provider: str, api_key: str, base_url: str):
      self.provider = provider
      self.api_key = api_key
      self.base_url = base_url
      self._client = httpx.AsyncClient(
          base_url=base_url,
          headers={"Authorization": f"Bearer {api_key}"},
          timeout=2.0,  # Hard timeout — never block call setup
      )

  async def lookup_by_phone(self, phone: str) -> CallerProfile | None:
      """Look up a caller by phone number. Returns None if not found."""
      try:
          if self.provider == "salesforce":
              return await self._salesforce_lookup(phone)
          elif self.provider == "hubspot":
              return await self._hubspot_lookup(phone)
          return None
      except httpx.TimeoutException:
          # CRM is slow — proceed without context rather than delay the call
          return None

  async def _salesforce_lookup(self, phone: str) -> CallerProfile | None:
      query = f"SELECT Id, Name, Account.Name, Email, Phone FROM Contact WHERE Phone = '{phone}'"
      resp = await self._client.get("/services/data/v59.0/query", params={"q": query})
      if resp.status_code != 200:
          return None
      records = resp.json().get("records", [])
      if not records:
          return None
      contact = records[0]
      return CallerProfile(
          name=contact["Name"],
          company=contact.get("Account", {}).get("Name", "Unknown"),
          email=contact.get("Email", ""),
          phone=phone,
          account_id=contact["Id"],
          lifetime_value=0.0,
          open_tickets=0,
          last_contact="",
          notes="",
          priority="standard",
      )

  async def _hubspot_lookup(self, phone: str) -> CallerProfile | None:
      resp = await self._client.post(
          "/crm/v3/objects/contacts/search",
          json={
              "filterGroups": [{
                  "filters": [{
                      "propertyName": "phone",
                      "operator": "EQ",
                      "value": phone,
                  }]
              }],
              "properties": ["firstname", "lastname", "company", "email", "phone"],
          },
      )
      if resp.status_code != 200:
          return None
      results = resp.json().get("results", [])
      if not results:
          return None
      props = results[0]["properties"]
      return CallerProfile(
          name=f"{props.get('firstname', '')} {props.get('lastname', '')}".strip(),
          company=props.get("company", "Unknown"),
          email=props.get("email", ""),
          phone=phone,
          account_id=results[0]["id"],
          lifetime_value=0.0,
          open_tickets=0,
          last_contact="",
          notes="",
          priority="standard",
      )

Never block call setup on CRM lookups

If the CRM is slow or down, proceed without context. A 2-second timeout is generous — if the lookup has not returned by then, greet the caller normally and attempt the lookup in the background. A delayed greeting is worse than a generic one.

Screen pop: enriching the AI agent

For AI agents, "screen pop" means injecting the caller profile into the agent's instructions before the conversation starts. The agent knows the caller's name, history, and open issues from the first word.

enriched_agent.pypython
from livekit.agents import AgentServer, Agent, AgentSession

server = AgentServer()


def build_context_instructions(profile: CallerProfile | None) -> str:
  """Generate agent instructions enriched with caller context."""
  base = """You are a customer service agent for Acme Corp. Be helpful,
  professional, and concise. Never use markdown or emojis."""

  if profile is None:
      return base + " You do not have prior information about this caller."

  return base + f"""

  CALLER CONTEXT (do not read this verbatim, use it to personalize):
  - Name: {profile.name}
  - Company: {profile.company}
  - Account ID: {profile.account_id}
  - Priority: {profile.priority}
  - Open tickets: {profile.open_tickets}
  - Last contact: {profile.last_contact}
  - Notes: {profile.notes}

  Greet {profile.name.split()[0]} by first name. If they have open tickets,
  ask if they are calling about an existing issue before starting fresh."""


@server.rtc_session
async def entrypoint(session: AgentSession):
  crm = CRMLookup(provider="salesforce", api_key="...", base_url="...")
  caller_phone = session.room.metadata  # Phone number passed in room metadata

  profile = await crm.lookup_by_phone(caller_phone)
  instructions = build_context_instructions(profile)

  await session.start(
      agent=Agent(instructions=instructions, tools=[...]),
      room=session.room,
      userdata={"profile": profile},
  )
What's happening

AI screen pop is more powerful than traditional screen pop for human agents. A human agent glances at a sidebar and might miss details. An AI agent has the entire caller profile woven into its instructions — it will never forget to mention an open ticket or fail to greet a VIP by name. The context becomes part of the agent's behavior, not just information displayed nearby.

Logging conversations back to the CRM

After each call, write the outcome back to the CRM. This closes the loop: the next time this caller phones in, the agent has the full history including today's call.

crmLogger.tstypescript
interface CallLog {
callerId: string;
accountId: string;
duration: number;
department: string;
outcome: "resolved" | "escalated" | "callback_scheduled" | "abandoned";
summary: string;
agentType: "ai" | "human";
}

async function logToSalesforce(log: CallLog, accessToken: string): Promise<void> {
const taskBody = {
  Subject: `Call: ${log.outcome} (${log.department})`,
  Description: log.summary,
  WhoId: log.accountId,
  Status: "Completed",
  Type: "Call",
  CallDurationInSeconds: log.duration,
  CallType: "Inbound",
};

await fetch("https://your-instance.salesforce.com/services/data/v59.0/sobjects/Task", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(taskBody),
});
}

async function logToHubSpot(log: CallLog, apiKey: string): Promise<void> {
const engagement = {
  properties: {
    hs_call_title: `Call: ${log.outcome}`,
    hs_call_body: log.summary,
    hs_call_duration: String(log.duration * 1000),
    hs_call_status: "COMPLETED",
    hs_call_direction: "INBOUND",
  },
  associations: [{
    to: { id: log.accountId },
    types: [{ associationCategory: "HUBSPOT_DEFINED", associationTypeId: 194 }],
  }],
};

await fetch("https://api.hubspot.com/crm/v3/objects/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(engagement),
});
}

Summarize with the LLM

Use the LLM to generate the call summary before logging it. After the call ends, send the full transcript with a prompt like "Summarize this customer service call in 2-3 sentences, including the issue, resolution, and any follow-up needed." This produces better summaries than rule-based extraction.

Course summary

Over this course, you have built an AI-native contact center architecture:

1

AI replaces the IVR

Natural language intent detection routes callers directly to the right department agent — no menus, no DTMF, no frustrated callers.

2

Multi-agent orchestration

A state machine coordinates specialized department agents, each with focused instructions and tools, with clean handoffs that preserve conversation context.

3

Seamless human escalation

When AI reaches its limits, the handoff to a human agent includes an LLM-generated summary and full context — the caller never repeats themselves.

4

CRM-enriched conversations

Caller lookup before the first greeting, context injection into agent instructions, and conversation logging back to the CRM closes the feedback loop.

Test your knowledge

Question 1 of 3

Why should a CRM lookup timeout gracefully (proceeding without context) rather than blocking the call setup?

Concepts covered
CRM lookupCaller enrichmentConversation loggingSalesforceHubSpot