What You'll Build
If you've ever watched a real estate agent manually sift through 50 leads trying to figure out who's actually ready to buy, you know how much time gets wasted on tire-kickers. In this Claude API tutorial, I'm going to show you how to build a production-ready AI lead qualifier that scores real estate prospects automatically — no manual triage, no spreadsheets.
By the end, you'll have a working Python agent that asks the right questions, uses Claude's tool use feature to capture structured lead data, and spits out a qualified lead score with a summary. The whole thing runs in under 200 lines of code.
Prerequisites
- Python 3.9 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
- Basic understanding of what a REST API is
- A terminal or IDE you're comfortable in
Full Source Code Overview
The agent is structured in four main parts: the tool definitions, the agent class, the run loop, and the test scenarios. Each step below builds one of those pieces. The model we're using throughout is claude-sonnet-4-6 — it handles multi-turn conversations and tool use reliably without overcomplicating things.
Step 1: Install the Anthropic SDK and Set Up Your API Key
First, install the Anthropic Python SDK. This is the only external dependency you need for this project.
terminalpip install anthropic
Next, set your API key as an environment variable. Don't hardcode it in your source files — that's how keys get accidentally pushed to GitHub.
terminal (Mac/Linux)export ANTHROPIC_API_KEY="sk-ant-your-key-here"
$env:ANTHROPIC_API_KEY="sk-ant-your-key-here"
Create a new file called lead_qualifier.py in your project folder. That's where all the code we write in the next steps will live.
.env file with python-dotenv, that works too. Just make sure you call load_dotenv() before you initialize the Anthropic client.
Step 2: Define the Lead Qualification Tool Definitions
This is the part most Claude API tutorials skip over, and it's actually the most important piece. Tool definitions tell Claude what structured data it's allowed to collect and in what format. Think of them as the fields on a lead intake form — except Claude fills them in naturally during conversation.
We're defining four tools: property preferences, budget range, purchase timeline, and contact capture. Each one maps to a real qualification signal agents use every day.
lead_qualifier.pyimport anthropic
import json
import os
from datetime import datetime
# Tool definitions tell Claude what structured data to collect
LEAD_TOOLS = [
{
"name": "capture_property_preferences",
"description": (
"Capture the lead's property preferences including property type, "
"desired location, number of bedrooms, bathrooms, and must-have features."
),
"input_schema": {
"type": "object",
"properties": {
"property_type": {
"type": "string",
"enum": ["single_family", "condo", "townhouse", "multi_family", "land", "commercial"],
"description": "Type of property the lead is looking for"
},
"location": {
"type": "string",
"description": "Desired city, neighborhood, or zip code"
},
"bedrooms_min": {
"type": "integer",
"description": "Minimum number of bedrooms required"
},
"bathrooms_min": {
"type": "number",
"description": "Minimum number of bathrooms required"
},
"must_have_features": {
"type": "array",
"items": {"type": "string"},
"description": "List of non-negotiable features like pool, garage, waterfront"
}
},
"required": ["property_type", "location"]
}
},
{
"name": "capture_budget",
"description": (
"Capture the lead's budget range, financing status, and down payment readiness. "
"This is a primary qualification signal."
),
"input_schema": {
"type": "object",
"properties": {
"budget_min": {
"type": "number",
"description": "Minimum budget in USD"
},
"budget_max": {
"type": "number",
"description": "Maximum budget in USD"
},
"financing_status": {
"type": "string",
"enum": ["pre_approved", "pre_qualified", "cash_buyer", "needs_financing", "unknown"],
"description": "Current mortgage or financing status"
},
"down_payment_ready": {
"type": "boolean",
"description": "Whether the lead has a down payment ready"
}
},
"required": ["budget_max", "financing_status"]
}
},
{
"name": "capture_timeline",
"description": (
"Capture when the lead wants to purchase and whether they have an urgency driver "
"like a lease ending or relocation deadline."
),
"input_schema": {
"type": "object",
"properties": {
"purchase_timeframe": {
"type": "string",
"enum": ["immediately", "1_3_months", "3_6_months", "6_12_months", "over_12_months", "just_browsing"],
"description": "When the lead plans to make a purchase"
},
"urgency_driver": {
"type": "string",
"description": "Reason for urgency if any, e.g. lease ending, job relocation, divorce"
},
"currently_renting": {
"type": "boolean",
"description": "Whether the lead is currently renting"
}
},
"required": ["purchase_timeframe"]
}
},
{
"name": "capture_contact_and_score",
"description": (
"Capture the lead's contact information and generate a final qualification score "
"from 1-10 based on the entire conversation. Call this tool last."
),
"input_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Lead's full name"
},
"email": {
"type": "string",
"description": "Lead's email address"
},
"phone": {
"type": "string",
"description": "Lead's phone number"
},
"qualification_score": {
"type": "integer",
"description": "Score from 1-10 based on budget, timeline, and motivation. 8-10 = hot lead, 5-7 = warm, 1-4 = cold"
},
"score_reasoning": {
"type": "string",
"description": "Brief explanation of why this score was assigned"
},
"recommended_action": {
"type": "string",
"enum": ["schedule_showing", "send_listings", "add_to_drip", "follow_up_in_30_days", "disqualify"],
"description": "Recommended next action for the agent"
}
},
"required": ["qualification_score", "score_reasoning", "recommended_action"]
}
}
]Each tool has a clear description that Claude reads to decide when to use it, and an input_schema that enforces the data shape. The enum fields are especially useful here — they constrain Claude's output so your downstream systems don't break on unexpected strings.
Step 3: Create the Lead Qualifier Agent Class
Now we build the agent class that wraps the Anthropic client and manages conversation state. The key design decision here is keeping the message history in memory so Claude has full context across the entire conversation — that's what makes the scoring accurate at the end.
lead_qualifier.py (continued)class LeadQualifierAgent:
def __init__(self):
# Initialize the Anthropic client — picks up ANTHROPIC_API_KEY from environment
self.client = anthropic.Anthropic()
self.model = "claude-sonnet-4-6"
self.messages = []
self.collected_data = {} # Stores all tool outputs for final summary
self.qualification_complete = False
# System prompt shapes Claude's persona and qualification strategy
self.system_prompt = """You are a professional real estate lead qualification assistant
for a Southwest Florida real estate agency. Your job is to have a friendly, natural conversation
with potential buyers and qualify them by collecting key information.
Follow this qualification order:
1. Greet them and learn what kind of property they're looking for — use capture_property_preferences
2. Understand their budget and financing situation — use capture_budget
3. Ask about their timeline and urgency — use capture_timeline
4. Get their contact info and produce a final score — use capture_contact_and_score
Important rules:
- Ask one or two questions at a time, never fire off a list of 6 questions
- Sound warm and human, not like a chatbot running a checklist
- If someone is evasive about budget, gently probe — it's the most important qualifier
- Use the tools as data is naturally revealed in conversation, not all at once at the end
- After calling capture_contact_and_score, say a warm closing line and stop"""
def add_message(self, role: str, content):
"""Append a message to the conversation history."""
self.messages.append({"role": role, "content": content})
def process_tool_call(self, tool_name: str, tool_input: dict) -> str:
"""
Handle tool execution. In production you'd write this to a CRM.
Here we store it in memory and return a confirmation.
"""
self.collected_data[tool_name] = tool_input
timestamp = datetime.now().strftime("%H:%M:%S")
# Pretty-print the captured data so we can see it in the console
print(f"\n 🔧 Tool called: {tool_name} at {timestamp}")
print(f" 📋 Data captured: {json.dumps(tool_input, indent=4)}\n")
return json.dumps({"status": "success", "captured": tool_input})
def get_lead_summary(self) -> dict:
"""Build a final summary dict from all collected tool data."""
summary = {
"timestamp": datetime.now().isoformat(),
"property_preferences": self.collected_data.get("capture_property_preferences", {}),
"budget": self.collected_data.get("capture_budget", {}),
"timeline": self.collected_data.get("capture_timeline", {}),
"contact_and_score": self.collected_data.get("capture_contact_and_score", {})
}
return summaryStep 4: Build the Main Run Loop with Tool Use
This is where the real estate lead scoring automation actually happens. The run loop sends messages to Claude, checks if Claude wants to use a tool, executes the tool, and feeds the result back — all in a tight while loop until qualification is complete.
The pattern here is standard Claude tool use: you send messages, get back either a text response or a tool_use block, handle whichever you got, and continue. Don't skip the role management — getting the message roles wrong is the number one cause of API errors with multi-turn tool use.
def chat(self, user_input: str) -> str:
"""
Send a user message and run the tool loop until Claude
produces a plain text response to return to the user.
"""
self.add_message("user", user_input)
while True:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.system_prompt,
tools=LEAD_TOOLS,
messages=self.messages
)
# Check if Claude wants to use one or more tools
if response.stop_reason == "tool_use":
# Add Claude's response (which contains tool_use blocks) to history
self.add_message("assistant", response.content)
# Process every tool call in this response turn
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_result = self.process_tool_call(block.name, block.input)
# Mark qualification complete when the final scoring tool runs
if block.name == "capture_contact_and_score":
self.qualification_complete = True
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": tool_result
})
# Feed all tool results back to Claude in a single user turn
self.add_message("user", tool_results)
else:
# Claude produced a plain text response — extract and return it
assistant_text = ""
for block in response.content:
if hasattr(block, "text"):
assistant_text += block.text
self.add_message("assistant", response.content)
return assistant_text
def start_conversation(self) -> str:
"""Kick off the qualification conversation with a greeting prompt."""
return self.chat("Hello, I'm interested in buying a property.")
def run_lead_qualifier():
"""Main entry point — runs an interactive lead qualification session."""
print("\n" + "="*60)
print(" Naples AI — Real Estate Lead Qualifier")
print(" Powered by Claude claude-sonnet-4-6")
print("="*60 + "\n")
agent = LeadQualifierAgent()
# Start the conversation automatically
opening = agent.start_conversation()
print(f"Agent: {opening}\n")
# Interactive loop — runs until qualification is complete
while not agent.qualification_complete:
user_input = input("You: ").strip()
if not user_input:
continue
response = agent.chat(user_input)
print(f"\nAgent: {response}\n")
if agent.qualification_complete:
break
# Print the final structured lead summary
print("\n" + "="*60)
print(" FINAL LEAD SUMMARY")
print("="*60)
summary = agent.get_lead_summary()
print(json.dumps(summary, indent=2))
print("\n✅ Lead qualification complete. Data ready for CRM import.\n")
if __name__ == "__main__":
run_lead_qualifier()Step 5: Test with Real Estate Lead Scenarios
Run the script and walk through a test conversation. Here's an example of what a hot lead qualification looks like end-to-end, so you know what to expect from your output.
terminalpython lead_qualifier.py
Here's a realistic sample conversation and the structured output it produces:
sample output============================================================
Naples AI — Real Estate Lead Qualifier
Powered by Claude claude-sonnet-4-6
============================================================
Agent: Hi there! Welcome — I'd love to help you find the perfect property
in Southwest Florida. To point you in the right direction, can you tell me
what kind of home you're envisioning? Are you thinking single-family,
a condo, or something else?
You: We're looking for a single family home, probably in Naples or Bonita Springs.
Agent: Great choices — both have some beautiful neighborhoods. How many
bedrooms are you thinking, and are there any must-haves like a pool or
waterfront access?
🔧 Tool called: capture_property_preferences at 14:32:11
📋 Data captured: {
"property_type": "single_family",
"location": "Naples or Bonita Springs, FL",
"bedrooms_min": 3,
"bathrooms_min": 2,
"must_have_features": ["pool", "3-car garage"]
}
You: At least 3 beds, 2 baths. A pool would be great and we'd love a 3-car garage.
Agent: Perfect. That really narrows things down nicely. What's the budget
range you're working with, and have you connected with a lender yet?
You: We're pre-approved up to $950,000. We have 20% down ready to go.
🔧 Tool called: capture_budget at 14:32:38
📋 Data captured: {
"budget_min": 700000,
"budget_max": 950000,
"financing_status": "pre_approved",
"down_payment_ready": true
}
Agent: That's a strong position to be in — pre-approved with a down payment
ready puts you ahead of a lot of buyers in this market. How soon are you
hoping to be in a new home? Is there anything driving the timing?
You: Our lease is up in August so ideally we'd close by end of July. We're
pretty motivated to move quickly.
🔧 Tool called: capture_timeline at 14:32:59
📋 Data captured: {
"purchase_timeframe": "1_3_months",
"urgency_driver": "Lease ending in August",
"currently_renting": true
}
Agent: A July close is definitely doable with the right property. I'd love
to get you connected with one of our agents right away. Can I grab your
name, email, and a phone number?
You: Sure! It's Maria and David Chen. [email protected], 239-555-0182.
🔧 Tool called: capture_contact_and_score at 14:33:21
📋 Data captured: {
"name": "Maria and David Chen",
"email": "[email protected]",
"phone": "239-555-0182",
"qualification_score": 9,
"score_reasoning": "Pre-approved at $950K, 20% down ready, clear urgency driver (lease ending August), specific property criteria, motivated timeline of 1-3 months. Classic hot lead.",
"recommended_action": "schedule_showing"
}
Agent: Wonderful, Maria and David! I've got everything I need. One of our
agents will be reaching out within the hour to schedule some showings.
You're in great shape — let's find you that perfect home before August!
============================================================
FINAL LEAD SUMMARY
============================================================
{
"timestamp": "2026-05-17T14:33:24.881203",
"property_preferences": {
"property_type": "single_family",
"location": "Naples or Bonita Springs, FL",
"bedrooms_min": 3,
"bathrooms_min": 2,
"must_have_features": ["pool", "3-car garage"]
},
"budget": {
"budget_min": 700000,
"budget_max": 950000,
"financing_status": "pre_approved",
"down_payment_ready": true
},
"timeline": {
"purchase_timeframe": "1_3_months",
"urgency_driver": "Lease ending in August",
"currently_renting": true
},
"contact_and_score": {
"name": "Maria and David Chen",
"email": "[email protected]",
"phone": "239-555-0182",
"qualification_score": 9,
"score_reasoning": "Pre-approved at $950K, 20% down ready, clear urgency driver (lease ending August), specific property criteria, motivated timeline of 1-3 months. Classic hot lead.",
"recommended_action": "schedule_showing"
}
}
✅ Lead qualification complete. Data ready for CRM import.How It Works
Here's the plain-English version of what's happening under the hood. When you send a message to Claude with tools defined, Claude can choose to respond with a tool_use block instead of plain text. That block contains the tool name and the structured data it extracted — like a form being filled in automatically mid-conversation.
Our run loop catches those tool blocks, calls process_tool_call(), and sends the results back to Claude as a tool_result message. Claude uses that confirmation to continue the conversation naturally, without re-asking for information it already captured. The whole loop repeats until Claude produces a plain text response with no tool calls, at which point we return that text to the user.
The scoring happens in the final capture_contact_and_score tool — Claude has the full conversation in its context window at that point, so it scores the lead based on everything said, not just the last message. That's what makes