← Back to Blog

If you're running a real estate business in Southwest Florida, you already know the problem: your team spends half the day chasing leads that were never serious buyers to begin with. Manually qualifying every inbound inquiry — sorting the curious browsers from the motivated buyers with real budgets — is slow, inconsistent, and expensive. This tutorial shows you exactly how to fix that with a Claude API lead qualifier agent built in Python.

By the end of this walkthrough, you'll have a working AI agent that automatically extracts contact details, property preferences, intent signals, and budget information from raw lead conversations — then scores each lead and returns a structured qualification report. No fluff, no theory. Just code that runs.

📦 Full Source Code Note: The complete working agent (~150 lines) is assembled across Steps 1–5 below. Each step builds on the last, so if you want the full file in one place, copy each snippet in order into a single file called lead_qualifier.py. The full runnable version will be complete by the end of Step 5.

1. What You'll Build

You're building a Python-based AI agent that takes a raw lead description — the kind of messy notes your sales team types into a CRM — and runs it through Claude's tool use system to extract structured data across four dimensions: contact info, property preferences, purchase intent, and budget.

The agent scores each lead from 0–100 and returns a clean JSON report your team can act on immediately. It uses the Anthropic SDK with claude-sonnet-4-6, streaming responses, and a proper agentic loop that handles multi-turn tool calls automatically.

2. Prerequisites

  • Python 3.10 or higher
  • An Anthropic API key (get one at console.anthropic.com)
  • anthropic Python SDK installed (pip install anthropic)
  • Basic Python knowledge — you don't need to be an AI expert
  • A terminal and a text editor (VS Code works great)

3. Full Source Code

💡 How to use this tutorial: The code below is split into logical steps so you understand what each part does. Paste all snippets into one file named lead_qualifier.py, in the order they appear, and you'll have a fully working agent. Every snippet is syntactically correct and tested.

4. Step 1: Set Up Your Claude API Client and Environment

First, let's install the SDK and set up your API key. Never hardcode your key in source files — use an environment variable instead.

Run this in your terminal to install the dependency and export your key:

terminal
pip install anthropic

# Mac/Linux
export ANTHROPIC_API_KEY="sk-ant-your-key-here"

# Windows PowerShell
$env:ANTHROPIC_API_KEY="sk-ant-your-key-here"

Now create your file and set up the client. This is the foundation everything else plugs into:

lead_qualifier.py
import os
import json
import anthropic

# Initialize the Anthropic client using the ANTHROPIC_API_KEY env variable
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4-6"

# This will hold our conversation turns for the agentic loop
conversation_history = []

That's all you need to start. The client automatically reads your API key from the environment, which is the right way to handle secrets in any production codebase.

5. Step 2: Define Lead Qualification Tools and Schemas

This is where the magic happens. Claude's tool use feature lets you define structured functions that the model can call when it identifies relevant information. Think of it like handing Claude a set of forms to fill out as it reads the lead.

We're defining four tools: one for each qualification dimension. Each tool has a strict JSON schema so the output is always machine-readable.

lead_qualifier.py (continued)
# Tool definitions tell Claude what structured data to extract
QUALIFICATION_TOOLS = [
    {
        "name": "capture_contact_info",
        "description": (
            "Extract contact information for the lead including name, "
            "email, phone number, and preferred contact method."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "full_name": {
                    "type": "string",
                    "description": "Full name of the lead"
                },
                "email": {
                    "type": "string",
                    "description": "Email address if provided"
                },
                "phone": {
                    "type": "string",
                    "description": "Phone number if provided"
                },
                "preferred_contact": {
                    "type": "string",
                    "enum": ["email", "phone", "text", "unknown"],
                    "description": "Preferred method of contact"
                }
            },
            "required": ["full_name", "preferred_contact"]
        }
    },
    {
        "name": "capture_property_preferences",
        "description": (
            "Extract what type of property the lead is looking for, "
            "including location, size, property type, and must-have features."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "property_type": {
                    "type": "string",
                    "enum": ["single_family", "condo", "townhouse", "land", "commercial", "unknown"],
                    "description": "Type of property the lead wants"
                },
                "target_location": {
                    "type": "string",
                    "description": "Desired city, neighborhood, or zip code"
                },
                "bedrooms_min": {
                    "type": "integer",
                    "description": "Minimum number of bedrooms needed"
                },
                "bathrooms_min": {
                    "type": "number",
                    "description": "Minimum number of bathrooms needed"
                },
                "must_have_features": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Non-negotiable features like pool, garage, waterfront"
                },
                "square_footage_min": {
                    "type": "integer",
                    "description": "Minimum square footage if mentioned"
                }
            },
            "required": ["property_type", "target_location"]
        }
    },
    {
        "name": "capture_purchase_intent",
        "description": (
            "Assess how serious and motivated the lead is about purchasing, "
            "including their timeline, whether they are pre-approved, "
            "and whether they are currently working with another agent."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "timeline": {
                    "type": "string",
                    "enum": ["immediate", "1_to_3_months", "3_to_6_months", "6_plus_months", "unknown"],
                    "description": "How soon the lead wants to purchase"
                },
                "is_pre_approved": {
                    "type": "boolean",
                    "description": "Whether the lead has mortgage pre-approval"
                },
                "has_existing_agent": {
                    "type": "boolean",
                    "description": "Whether the lead is already working with another agent"
                },
                "motivation_notes": {
                    "type": "string",
                    "description": "Any context about why they are buying — relocation, investment, lifestyle"
                },
                "is_also_selling": {
                    "type": "boolean",
                    "description": "Whether the lead also needs to sell an existing property"
                }
            },
            "required": ["timeline", "is_pre_approved", "has_existing_agent"]
        }
    },
    {
        "name": "capture_budget_info",
        "description": (
            "Extract the lead's budget range, financing method, "
            "and any flexibility signals around price."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "budget_min": {
                    "type": "integer",
                    "description": "Minimum budget in USD"
                },
                "budget_max": {
                    "type": "integer",
                    "description": "Maximum budget in USD"
                },
                "financing_type": {
                    "type": "string",
                    "enum": ["cash", "conventional", "fha", "va", "unknown"],
                    "description": "How the lead plans to finance the purchase"
                },
                "is_budget_flexible": {
                    "type": "boolean",
                    "description": "Whether the lead indicated flexibility above their stated max"
                },
                "down_payment_ready": {
                    "type": "boolean",
                    "description": "Whether the lead has confirmed they have a down payment ready"
                }
            },
            "required": ["budget_max", "financing_type"]
        }
    }
]
⚠️ Why schemas matter: Without strict JSON schemas, Claude might return budget data as "around $500k" instead of 500000. The schema forces structured, typed output your code can actually use without fragile string parsing.

6. Step 3: Build the Agent Loop with tool_use

This is the core of the agent. The loop sends the lead description to Claude, receives tool call requests, executes the tools, feeds the results back, and keeps going until Claude signals it's done.

Claude will often call multiple tools in a single pass — that's expected behavior. The loop handles all of it.

lead_qualifier.py (continued)
def run_qualification_agent(lead_description: str) -> dict:
    """
    Run the full agentic loop to qualify a lead.
    Returns a dict of all extracted tool results.
    """
    # Collected tool outputs stored by tool name
    tool_results_store = {}

    system_prompt = """You are an expert real estate lead qualification assistant.
Your job is to analyze lead descriptions and extract structured information
using the tools available to you. Be thorough — call all relevant tools
even if some information is incomplete or inferred. If a detail is not
mentioned, use your best judgment based on context clues or mark it unknown.
Always call all four tools for every lead."""

    messages = [
        {
            "role": "user",
            "content": f"Please qualify this real estate lead:\n\n{lead_description}"
        }
    ]

    # Agentic loop — runs until Claude stops calling tools
    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=4096,
            system=system_prompt,
            tools=QUALIFICATION_TOOLS,
            messages=messages
        )

        # Append Claude's response to the conversation
        messages.append({"role": "assistant", "content": response.content})

        # Check if we are done (no more tool calls)
        if response.stop_reason == "end_turn":
            break

        # Process any tool use blocks in the response
        tool_use_blocks = [
            block for block in response.content
            if block.type == "tool_use"
        ]

        if not tool_use_blocks:
            break

        # Build the tool results to send back to Claude
        tool_results = []
        for tool_use in tool_use_blocks:
            tool_name = tool_use.name
            tool_input = tool_use.input

            # Store the extracted data — this is our qualification output
            tool_results_store[tool_name] = tool_input

            # Send a success acknowledgment back to Claude for each tool call
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": json.dumps({"status": "captured", "data": tool_input})
            })

        # Add tool results back into the conversation so Claude can continue
        messages.append({
            "role": "user",
            "content": tool_results
        })

    return tool_results_store

The while True loop is intentional. Claude might call one tool, then decide it needs more information before calling another. We keep the loop running until stop_reason is "end_turn", which is Claude's signal that it's finished.

7. Step 4: Implement Lead Scoring Logic

Raw extracted data is useful, but your sales team needs to know who to call first. This scoring function turns the structured tool outputs into a numeric score from 0–100.

The scoring weights are based on what actually drives close rates in real estate: pre-approval status, timeline, and budget clarity matter most.

lead_qualifier.py (continued)
def score_lead(tool_results: dict) -> dict:
    """
    Score a lead from 0-100 based on qualification signals.
    Returns a score dict with breakdown and tier label.
    """
    score = 0
    breakdown = {}

    # --- Contact completeness (max 15 points) ---
    contact = tool_results.get("capture_contact_info", {})
    contact_score = 0
    if contact.get("email"):
        contact_score += 7
    if contact.get("phone"):
        contact_score += 8
    score += contact_score
    breakdown["contact_completeness"] = contact_score

    # --- Purchase intent signals (max 40 points) ---
    intent = tool_results.get("capture_purchase_intent", {})
    intent_score = 0

    timeline = intent.get("timeline", "unknown")
    timeline_points = {
        "immediate": 20,
        "1_to_3_months": 15,
        "3_to_6_months": 8,
        "6_plus_months": 3,
        "unknown": 0
    }
    intent_score += timeline_points.get(timeline, 0)

    # Pre-approval is the strongest buying signal
    if intent.get("is_pre_approved"):
        intent_score += 15

    # Already has an agent is a negative signal
    if intent.get("has_existing_agent"):
        intent_score -= 10

    intent_score = max(0, intent_score)
    score += intent_score
    breakdown["purchase_intent"] = intent_score

    # --- Budget clarity (max 30 points) ---
    budget = tool_results.get("capture_budget_info", {})
    budget_score = 0

    if budget.get("budget_max"):
        budget_score += 15
    if budget.get("budget_min"):
        budget_score += 5

    financing = budget.get("financing_type", "unknown")
    if financing == "cash":
        budget_score += 10
    elif financing in ("conventional", "va", "fha"):
        budget_score += 6

    if budget.get("down_payment_ready"):
        budget_score += 5

    budget_score = min(30, budget_score)
    score += budget_score
    breakdown["budget_clarity"] = budget_score

    # --- Property specificity (max 15 points) ---
    prefs = tool_results.get("capture_property_preferences", {})
    property_score = 0

    if prefs.get("target_location") and prefs["target_location"].lower() != "unknown":
        property_score += 7
    if prefs.get("bedrooms_min"):
        property_score += 4
    if prefs.get("must_have_features"):
        property_score += 4

    score += property_score
    breakdown["property_specificity"] = property_score

    # Clamp the final score to 0-100
    final_score = max(0, min(100, score))

    # Assign a human-readable tier
    if final_score >= 75:
        tier = "HOT"
    elif final_score >= 50:
        tier = "WARM"
    elif final_score >= 25:
        tier = "COLD"
    else:
        tier = "UNQUALIFIED"

    return {
        "score": final_score,
        "tier": tier,
        "breakdown": breakdown
    }
💡 Customize the weights: These scoring weights are a solid starting point, but every market is different. Naples luxury waterfront buyers behave differently than first-time condo buyers in Bonita Springs. Adjust the timeline_points and bonus values based on your own close-rate data.

8. Step 5: Parse and Format Qualified Lead Output

The last piece ties everything together: a function that calls the agent, scores the result, and formats a clean report you can print, log, or push into your CRM via API.

This is also where we add a realistic sample lead and run the whole thing end to end.

lead_qualifier.py (continued)
def format_qualification_report(
    lead_description: str,
    tool_results: dict,
    score_data: dict
) -> str:
    """
    Format a human-readable qualification report from extracted data.
    """
    contact = tool_results.get("capture_contact_info", {})
    prefs = tool_results.get("capture_property_preferences", {})
    intent = tool_results.get("capture_purchase_intent", {})
    budget = tool_results.get("capture_budget_info", {})

    lines = [
        "=" * 55,
        f"  LEAD QUALIFICATION REPORT",
        f"  Score: {score_data['score']}/100  |  Tier: {score_data['tier']}",
        "=" * 55,
        "",
        "CONTACT",
        f"  Name:    {contact.get('full_name', 'Unknown')}",
        f"  Email:   {contact.get('email', 'Not provided')}",
        f"  Phone:   {contact.get('phone', 'Not provided')}",
        f"  Prefers: {contact.get('preferred_contact', 'unknown')}",
        "",
        "PROPERTY PREFERENCES",
        f"  Type:     {prefs.get('property_type', 'unknown')}",
        f"  Location: {prefs.get('target_location', 'unknown')}",
        f"  Beds:     {prefs.get('bedrooms_min', 'any')}+",
        f"  Baths:    {prefs.get('bathrooms_min', 'any')}+",
        f"  Must-haves: {', '.join(prefs.get('must_have_features', [])) or 'None stated'}",
        "",
        "INTENT",
        f"  Timeline:     {intent.get('timeline', 'unknown')}",
        f"  Pre-approved: {'Yes' if intent.get('is_pre_approved') else 'No'}",
        f"  Has agent:    {'Yes' if intent.get('has_existing_agent') else 'No'}",
        f"  Notes:        {intent.get('motivation_notes', 'None')}",
        "",
        "BUDGET",
        f"  Range:     ${budget.get('budget_min', 0):,} – ${budget.get('budget_max', 0):,}",
        f"  Financing: {budget.get('financing_type', 'unknown')}",
        f"  Down pmt:  {'Ready' if budget.get('down_payment_ready') else 'Not confirmed'}",
        f"  Flexible:  {'Yes' if budget.get('is_budget_flexible') else 'No'}",
        "",
        "SCORE BREAKDOWN",
    ]

    for category, points in score_data["breakdown"].items():
        lines.append(f"  {category.replace('_', ' ').title()}: {points} pts")

    lines.append("=" * 55)
    return "\n".join(lines)


def qualify_lead(lead_description: str) -> None:
    """
    Main entry point — qualifies a lead and prints the full report.
    """
    print(f"\nQualifying lead... (this may take a few seconds)\n")

    # Run the agentic loop to extract structured data
    tool_results = run_qualification_agent(lead_description)

    # Score the extracted data
    score_data = score_lead(tool_results)

    # Format and print the report
    report = format_qualification_report(lead_description, tool_results, score_data)
    print(report)

    # Also dump the raw JSON for CRM integration
    print("\nRAW JSON OUTPUT (for CRM/webhook):")
    output = {
        "score": score_data["score"],
        "tier": score_data["tier"],
        "breakdown": score_data["breakdown"],
        "extracted_data": tool_results
    }
    print(json.dumps(output, indent=2))


# --- Run it with a sample lead ---
if __name__ == "__main__":
    sample_lead = """
    Hey, my name is Maria Gonzalez. I found your listing on Zillow for that
    waterfront place in Naples and I'm really interested. My husband and I
    are looking to relocate from Chicago — he just got a remote job so we
    want to make the move within the next 6-8 weeks. We've been pre-approved
    for a conventional loan up to $1.2 million but honestly if the right place
    came up we could probably go a bit higher. We're looking for at least
    3 bedrooms, 2 bathrooms, and a pool is non-negotiable. We're not working
    with an agent yet. You can reach me by email at [email protected] or