← Back to Blog

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 Note: The complete working code is split across the steps below so you can understand each piece as you build it. Every snippet connects to the next — by Step 5, you'll have the full agent running. Copy each block in order and you're good to go.

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.

terminal
pip 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"
terminal (Windows PowerShell)
$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.

💡 Tip: If you're using a .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.py
import 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 summary

Step 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.

lead_qualifier.py (continued)
    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.

terminal
python 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