Developers

Build an AI Calling Agent with Claude and the RizzDial API

A working Python tutorial: wrap the RizzDial REST API as Claude tools so Claude decides when to list agents, buy a number, place a call and read call history, with a human confirmation guardrail.

By James Hill ·

By the end of this you have a Python script where you type "call the lead I just added and confirm Thursday" and Claude works out which RizzDial endpoints to hit, asks you to approve the dial, and reports what happened on the call.

Every endpoint below was checked against the live OpenAPI 3.0.3 spec at https://app.rizzdial.com/api/docs/spec on 2026-09-24, and the Claude tool-use shapes against the Claude API docs the same day.

Answer first: Wrap each RizzDial REST endpoint as a Claude tool with a name, a description and an input_schema. Claude returns a tool_use block when it wants one; your code executes the HTTP request and replies with a tool_result block. Placing a call is deliberately two steps: GET /api/ai/agent/call-endpoint returns that agent's unique call URL, then you POST the phone number to that URL. Gate the second step behind a human yes and a consent check, because that step makes a phone ring.

What you need before any code runs

The RizzDial API is available to RizzDial customers. If you are already a customer, open Profile, then Settings, then Personal Access Tokens, and create a token. Tokens can be set to expire or to never expire. If you are not a customer yet, book a call to get API access.

Everything talks to https://app.rizzdial.com and authenticates with a bearer header:

Authorization: Bearer YOUR_TOKEN

The full surface is 224 paths in the OpenAPI spec, browsable with Try it out at https://app.rizzdial.com/api/docs. OAuth 2.0 clients exist too, for apps that act on behalf of other users; the RizzDial API docs cover that flow. A personal access token is the right choice for a script you run yourself.

You also need an Anthropic API key in ANTHROPIC_API_KEY, and two packages:

pip install anthropic requests
export RIZZDIAL_TOKEN=YOUR_TOKEN
export ANTHROPIC_API_KEY=YOUR_ANTHROPIC_KEY

The seven calls we are wrapping

Tool RizzDial call Why Claude needs it
list_agents GET /api/ai/agent/list Find the agent_id to work with
create_agent POST /api/ai/agent/create Spin up a new voice agent from a prompt
search_numbers GET /api/ai/number/search Find a buyable number in an area code
purchase_number POST /api/ai/number/purchase Buy one of those numbers
assign_number POST /api/ai/number/assign Attach the number to the agent
place_call GET /api/ai/agent/call-endpoint, then POST to the returned URL Actually dial
list_calls GET /api/ai/call-history Read transcripts, summaries, sentiment

An agent with no outbound number assigned cannot dial, which is why assign_number sits in the list.

Step 1: a thin RizzDial client

No cleverness here. One function per endpoint, exact field names from the spec, including the spec's own spelling of weebhook_endpoint.

# rizzdial.py
import os
import requests

BASE_URL = "https://app.rizzdial.com"
TIMEOUT = 30


def _headers():
    return {
        "Authorization": "Bearer " + os.environ["RIZZDIAL_TOKEN"],
        "Accept": "application/json",
        "Content-Type": "application/json",
    }


def _get(path, params=None):
    r = requests.get(BASE_URL + path, headers=_headers(),
                     params=params or {}, timeout=TIMEOUT)
    r.raise_for_status()
    return r.json()


def _post(path, body):
    r = requests.post(BASE_URL + path, headers=_headers(),
                      json=body, timeout=TIMEOUT)
    r.raise_for_status()
    return r.json()


def list_agents(per_page=15):
    return _get("/api/ai/agent/list", {"per_page": per_page})


def create_agent(agent_name, general_prompt, weebhook_endpoint=None):
    body = {"agent_name": agent_name, "general_prompt": general_prompt}
    if weebhook_endpoint:
        body["weebhook_endpoint"] = weebhook_endpoint
        body["weebhook_endpoint_method"] = "POST"
    return _post("/api/ai/agent/create", body)


def search_numbers(area_code, provider="Twilio", search_by="AreaCode"):
    return _get("/api/ai/number/search", {
        "provider": provider,
        "area_code": area_code,
        "search_by": search_by,
    })


def purchase_number(phone_token, nickname, provider="Twilio"):
    return _post("/api/ai/number/purchase", {
        "phone_token": phone_token,
        "provider": provider,
        "nickname": nickname,
    })


def assign_number(number_id, agent_id, direction="outbound"):
    return _post("/api/ai/number/assign", {
        "number_id": number_id,
        "agent_id": agent_id,
        "type": direction,
    })


def list_calls(agent_id=None, start_date=None, per_page=15):
    params = {"per_page": per_page}
    if agent_id:
        params["agent_id"] = agent_id
    if start_date:
        params["start_date"] = start_date
    return _get("/api/ai/call-history", params)

Two details worth pinning down. phone_token comes back from the search response and must reach the purchase call exactly as received. number_id from GET /api/ai/number/list is a base64 string, and so is id in call history records; pass them verbatim rather than decoding or casting them.

Step 2: placing a call is two steps

GET /api/ai/agent/call-endpoint?agent_id=YOUR_AGENT_ID returns data.endpoint, a per-agent URL under /webhooks/ai/create-phone-call/, along with data.Method and data.form_data listing the accepted fields. You then POST JSON to that URL with no bearer header. The token inside the path is the credential, so treat that URL exactly like a password: never log it, never put it in a client-side bundle.

def get_call_endpoint(agent_id):
    data = _get("/api/ai/agent/call-endpoint", {"agent_id": agent_id})
    return data["data"]["endpoint"]


def place_call(agent_id, phone_number, first_name=None, last_name=None,
               call_mode="outbound", extra=None):
    body = {"phone_number": phone_number, "call_mode": call_mode}
    if first_name:
        body["first_name"] = first_name
    if last_name:
        body["last_name"] = last_name
    if extra:
        body.update(extra)          # passed through as dynamic variables
    r = requests.post(get_call_endpoint(agent_id), json=body, timeout=TIMEOUT)
    r.raise_for_status()
    return r.json()

phone_number must be E.164, starting with a plus. A success looks like {"status": "success", "msg": "successfully make a call.", "call_id": "..."}. Common failures are an inactive agent, an agent with no number assigned, and an insufficient balance. Anything extra you put in extra reaches the agent as a dynamic variable, so a renewal_date key can be spoken on the call. Set call_mode to inbound instead and you get a dialSip address back rather than a ringing phone, which is how you hand an existing caller to the agent. The endpoint docs cover both tabs.

Step 3: describe the tools to Claude

Claude picks tools from their descriptions, so the description is the product here. Say what the tool does, what the inputs mean, and when not to use it.

# tools.py
TOOLS = [
    {
        "name": "list_agents",
        "description": (
            "List the AI voice agents on this RizzDial account. "
            "Call this first when the user names an agent in words "
            "rather than giving an agent_id."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "per_page": {"type": "integer",
                             "description": "Results per page, max 30."}
            },
            "required": [],
        },
    },
    {
        "name": "create_agent",
        "description": (
            "Create a new AI voice agent. general_prompt is the agent's "
            "whole personality and objective, so write it in full. "
            "weebhook_endpoint is an optional URL that receives the "
            "post-call payload when the call has been analyzed."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "agent_name": {"type": "string"},
                "general_prompt": {"type": "string"},
                "weebhook_endpoint": {"type": "string"},
            },
            "required": ["agent_name", "general_prompt"],
        },
    },
    {
        "name": "place_call",
        "description": (
            "Place one outbound AI call. Requires an agent that is active "
            "and has an outbound number assigned. This makes a real phone "
            "ring, so never call it speculatively and never call it for a "
            "number the user has not explicitly named."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "agent_id": {"type": "string"},
                "phone_number": {"type": "string",
                                 "description": "E.164, for example +13125550142"},
                "first_name": {"type": "string"},
                "last_name": {"type": "string"},
            },
            "required": ["agent_id", "phone_number"],
        },
    },
    {
        "name": "list_calls",
        "description": (
            "Read AI call history: summary, transcript, sentiment, "
            "duration, hangup reason and recording URL. Filter by "
            "agent_id and by start_date in Y-m-d form. There is no "
            "filter by call_id, so narrow by date and agent instead."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "agent_id": {"type": "string"},
                "start_date": {"type": "string"},
                "per_page": {"type": "integer"},
            },
            "required": [],
        },
    },
]

Add search_numbers, purchase_number and assign_number the same way, each description honest about its side effect.

Step 4: the loop, with a guardrail in the middle

The Claude API returns stop_reason: "tool_use" and one or more tool_use blocks carrying id, name and input. You reply with a user message whose content starts with the matching tool_result blocks. Keep the model id in one constant so this file does not age badly; check the current list in the Claude docs when you pin it.

# agent.py
import json
import anthropic
import rizzdial
from tools import TOOLS

CLAUDE_MODEL = "YOUR_CLAUDE_MODEL_ID"  # pin a current model id from the Claude docs
client = anthropic.Anthropic()

HANDLERS = {
    "list_agents": rizzdial.list_agents,
    "create_agent": rizzdial.create_agent,
    "search_numbers": rizzdial.search_numbers,
    "purchase_number": rizzdial.purchase_number,
    "assign_number": rizzdial.assign_number,
    "place_call": rizzdial.place_call,
    "list_calls": rizzdial.list_calls,
}

NEEDS_APPROVAL = {"place_call", "purchase_number", "create_agent"}

SYSTEM = (
    "You operate a RizzDial account through the supplied tools. "
    "Resolve names to ids with list_agents before acting. "
    "Never place a call unless the user gave you that phone number in "
    "this conversation. State what you are about to do before you do it."
)


def has_consent(phone_number):
    """Replace with a real lookup against your CRM or consent table."""
    return phone_number in load_consented_numbers()


def approve(block):
    if block.name == "place_call":
        number = block.input.get("phone_number", "")
        if not has_consent(number):
            print("No consent record for " + number)
            return False
    print("Claude wants " + block.name + ":")
    print(json.dumps(block.input, indent=2))
    return input("Approve? type yes: ").strip().lower() == "yes"


def run_tool(block):
    result = {"type": "tool_result", "tool_use_id": block.id}
    if block.name in NEEDS_APPROVAL and not approve(block):
        result["content"] = (
            "The operator declined this action. Do not retry it. "
            "Explain to the user what was blocked and why."
        )
        result["is_error"] = True
        return result
    try:
        result["content"] = json.dumps(HANDLERS[block.name](**block.input))
    except Exception as exc:
        result["content"] = "Tool failed: " + str(exc)
        result["is_error"] = True
    return result


def run(user_message):
    messages = [{"role": "user", "content": user_message}]
    while True:
        response = client.messages.create(
            model=CLAUDE_MODEL,
            max_tokens=2048,
            system=SYSTEM,
            tools=TOOLS,
            messages=messages,
        )
        if response.stop_reason != "tool_use":
            return "".join(b.text for b in response.content if b.type == "text")
        messages.append({"role": "assistant", "content": response.content})
        results = [run_tool(b) for b in response.content if b.type == "tool_use"]
        messages.append({"role": "user", "content": results})


if __name__ == "__main__":
    print(run("Call +13125550142 for Dana Reyes with the Renewals agent "
              "and confirm Thursday at 2pm."))

Three things make this loop safe rather than merely functional. The approval gate sits between the model and the side effect, so a wrong tool call costs you a keystroke and not a phone call. A declined action returns is_error: true with an instruction not to retry, which stops Claude looping on the same request. And tool results are data from an external system, so treat them as untrusted input rather than as instructions.

For production, swap the input() prompt for whatever your team already trusts: a Slack approval, or a policy that auto-approves only numbers already flagged as consented in your CRM.

Closing the loop on results

Two ways to learn what happened. Poll GET /api/ai/call-history filtered by agent_id and start_date, which returns records with call_id, call_duration, call_summary, call_transcript, user_sentiment, hangup_reason, call_successful and call_recording. Or set weebhook_endpoint at agent creation time and let RizzDial push the same fields to you once the call is analyzed, plus any arguments the agent collected during the conversation as extra top-level keys.

The same URL can be set later in the dashboard under the agent's Advanced tab.

The same flow, with no code at all

If you just want an assistant that can do this rather than an application that does it, skip the Python. RizzDial runs a remote MCP server with browser sign-in, so Claude Code, Codex or Grok can drive the same account directly: "list my AI agents", "create an outbound agent for lead follow-up", "show recent call history". Setup is one command plus an approval screen. See /mcp and the walkthrough in Add Phone Calls to Your AI Agent with MCP.

The rule of thumb: MCP when a human is in the chat, the REST API when software runs unattended.

Consent before you dial

The FCC's February 2024 declaratory ruling treats AI-generated voices as "artificial" voices under the TCPA, which means calls placed with them need prior express consent from the person you are calling. Read the ruling on fcc.gov and our TCPA and FCC compliance notes. RizzDial honors a do-not-call flag on the contact record, and does not check your list against the federal DNC registry or litigator lists; that screening is yours. This is not legal advice.

That is why has_consent is a real function in the code above rather than a comment. An autonomous agent with dialing tools will dial whatever you let it dial.

Frequently asked questions

Do I need an Anthropic key and a RizzDial token?

Yes, two credentials. The Anthropic key pays for Claude's reasoning. The RizzDial token authorizes the account actions. Neither substitutes for the other.

Can Claude place a call without me approving it?

Only if you build it that way. Nothing in the API forces a confirmation step; the NEEDS_APPROVAL set above is your code. Keep place_call and purchase_number inside it.

Why is weebhook_endpoint spelled like that?

That is the field name in the API, and the code has to match it exactly. Copy it as-is from the spec.

How do I find an agent_id?

GET /api/ai/agent/list returns your agents with their ids. Agent ids look like agent_abc123. Some other ids, such as number_id in the numbers endpoints, are base64 strings; pass those through untouched.

Can I filter call history by call_id?

No. The documented filters are per_page, agent_id, start_date, end_date, direction, user_sentiment, call_successful, from_number, to_number, hangup_reason, min_duration and max_duration. Narrow by agent and date, then match on call_id in your own code.

Which model should I use?

Whichever current Claude model you have tested against, pinned in the CLAUDE_MODEL constant. Tool-use quality matters more than raw speed here, because a mistaken tool call dials a stranger.


Build the client, wrap it as tools, put a human in front of the dial. Then let Claude handle the part where it works out which of the 224 endpoints it needs.

Get API access or book a call to talk through your build.


About RizzDial

RizzDial is the AI outbound sales workspace for teams on GoHighLevel. Power dialing, AI voice agents, SMS automation, and CRM workflows in one platform. Book a demo.