AgentCalls
Remote Streamable HTTP MCP server (OAuth 2.1, 43 tools) giving AI agents in Claude, ChatGPT, Cursor, VS Code and Windsurf a real phone number and voice: outbound calls with live transcripts, inbound answering in English and Arabic across 95 countries, caller memory, 1,500+ integrations mid-call, post-call summaries, confirmation guardrails and audit log. Endpoint: https://agentcalls.net/mcp
Hosted MCP Server
npx add-mcp 'https://agentcalls.net/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
Overview
The AgentCalls MCP server lets AI agents (Claude, ChatGPT, Cursor, Claude Code, your own agents) run your phone line through the Model Context Protocol: build and publish voice agents, get numbers in 95+ countries, place and answer calls, read transcripts and summaries, run campaigns, manage contacts, webhooks and billing. Paste one URL, sign in, done.
Endpoint: https://agentcalls.net/mcp. Sign-in is standard OAuth 2.1 with discovery, so any client that supports remote MCP servers connects with the URL alone. Hosts that support MCP Apps (Claude on web, desktop and mobile; ChatGPT) also get interactive cards for calls, agents, numbers, balance and campaigns.
| Client | Connect with | Cards |
|---|---|---|
| Claude (web, desktop, mobile) | URL + sign in | Yes |
| ChatGPT (developer mode) | URL + sign in | Yes |
| Claude Code | URL + sign in, or API key | Text results |
| Cursor | URL + sign in, or API key | Text results |
| Any MCP client or SDK | URL + sign in, or API key | Text results |
Connect in 60 seconds

Claude → Customize → Connectors → Add custom connector. Name: AgentCalls. URL: https://agentcalls.net/mcp. That is all.
Developer mode · New connector
Name
AgentCalls
MCP server URL
Authentication
OAuth
ChatGPT → Settings → Apps & Connectors → Developer mode → Create (illustration of the form).
Claude (web, desktop, mobile)
- Open Customize → Connectors → Add custom connector (Team/Enterprise: Organization settings → Connectors → Add → Custom).
- Name: AgentCalls. MCP server URL: https://agentcalls.net/mcp. Leave the OAuth client settings on their defaults ("Use Claude's published identity").
- Click Add, then Connect: an AgentCalls sign-in opens; pick the organization and the permissions, and approve.
- Ask Claude: "Where am I with AgentCalls?" — it calls get_started and shows the setup card.
ChatGPT (developer mode)
- Settings → Apps & Connectors → Advanced → turn on Developer mode (Plus, Pro, Business, Enterprise, Edu).
- Connectors → Create: Name AgentCalls, MCP server URL https://agentcalls.net/mcp, Authentication: OAuth. Save.
- Open a new chat, enable the AgentCalls connector from the tools menu and sign in when asked: pick the organization and approve.
Claude Code
- Add the server, then run /mcp inside Claude Code and choose Authenticate (a browser window opens). For CI or scripts, pass an API key header instead:
claude mcp add --transport http agentcalls https://agentcalls.net/mcp
# non-interactive alternative (API key from Settings → Developers):
claude mcp add --transport http agentcalls https://agentcalls.net/mcp \
--header "Authorization: Bearer $AGENTCALLS_API_KEY"
Cursor
- Add to.cursor/mcp.json (project) or ~/.cursor/mcp.json (global); Cursor signs in through the browser the first time. To skip the sign-in, add a
headersblock with an API key:
{
"mcpServers": {
"agentcalls": {
"url": "https://agentcalls.net/mcp"
}
}
}
// with an API key instead of sign-in:
{
"mcpServers": {
"agentcalls": {
"url": "https://agentcalls.net/mcp",
"headers": {
"Authorization": "Bearer ${env:AGENTCALLS_API_KEY}"
}
}
}
}
Any MCP client or SDK
- Streamable HTTP at https://agentcalls.net/mcp. Authorization: OAuth 2.1 (discovery at https://agentcalls.net/.well-known/oauth-protected-resource/mcp, PKCE, Client ID Metadata Documents or Dynamic Client Registration) or a fixed
Authorization: Bearer ak_live_…API key. No sessions: every request is independent.
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL("https://agentcalls.net/mcp"), {
authProvider: { token: async () => process.env.AGENTCALLS_API_KEY! },
}));
const { tools } = await client.listTools();
const status = await client.callTool({ name: "get_started", arguments: {} });
After connecting, the first thing to ask is “Where am I with AgentCalls?”: the assistant calls get_started and shows the setup card with the next steps.
How sign-in works
AgentCalls is its own OAuth 2.1 authorization server, so nothing is configured on the client side:
- The client calls the endpoint without a token and receives
401withWWW-Authenticate: Bearer resource_metadata=…(RFC 9728 protected resource metadata athttps://agentcalls.net/.well-known/oauth-protected-resource/mcp), which names the authorization server and the default permissions. - It reads the authorization server metadata (
/.well-known/oauth-authorization-server, RFC 8414) and identifies itself with its published Client ID Metadata Document (Claude, ChatGPT, Claude Code) or registers dynamically (RFC 7591; Cursor and others). - You land on the AgentCalls consent screen, signed in with your usual account (email or Google), pick the organization and the permission groups, and approve. PKCE (S256) protects the code; the client exchanges it at
/oauth/tokenfor an access token (1 hour) and a rotating refresh token (30 days). - Every tool call carries the token; a tool outside the granted permissions answers
403 insufficient_scope, and the client asks you to approve the missing permission (step-up) without losing the conversation.
Revoke a connection any time under Settings → Developers → Connected AI apps; its tokens stop working immediately. Administrators see every connection per organization.
Interactive cards
Tools marked card render an interactive view in hosts that support the MCP Apps extension (Claude web, desktop and mobile; ChatGPT). The views are self-contained HTML served by the server as ui://agentcalls/… resources; they follow the host theme (light and dark), the organization's language (English or Arabic) and never load anything from outside the host. Text-only clients get the same data as JSON.
Live call. start_call and get_call show who is on the line, the elapsed time and the live transcript, with an End call button (end_call after an inline confirmation). When the call ends the same card turns into the summary: outcome, sentiment, cost, recording link.
Call summary. The ended state of the call card.
Agent. get_agent, create_agent, update_agent and publish_agent render the draft: voice, hours, opening line, instructions, tools, with Publish and dashboard links.
Number search. search_numbers lists candidates with the monthly price; Buy asks for a confirmation in the card, then calls buy_number. Request-only countries get a Request button.
Balance. get_usage shows plan, minutes, wallet, numbers and lines; the top-up packs and plan button fetch hosted checkout links (nothing is charged in the card).
Campaign. get_campaign and set_campaign_status show progress and outcomes, refresh while running, and offer Pause, Resume and Cancel with confirmations.
Setup status. get_started: the first card in a conversation: plan, balance, counts and the next steps, each with a button that asks the assistant to do it.
Your numbers. list_numbers, get_number and assign_number: every number with its agent, price and status, pending requests, and Assign / Release buttons that ask the assistant to act.
Recent calls. list_calls: direction, status, outcome label, cost (or Pending while billing runs) and the plain-language summary; click a row to open it.
Live calls board. list_live_calls: calls on the line now with timers and End buttons; refreshes every few seconds.
Contacts. list_contacts, get_contact, upsert_contact and set_do_not_call: people, tags, what the agent remembers, Call and do-not-call buttons.
Integrations. list_integrations and get_integration_connect_link: connected tools with their actions; Connect fetches a sign-in link.
Webhooks. list_webhooks, create_webhook and test_webhook: endpoints, delivery dots, the signing secret once, and a test event.
Outbound rules. get_organization and update_outbound_settings: allowed countries, calling hours and days, consent line and the compliance state.
Previews run with sample data. In a host, the buttons call the server's tools through the host (tools/call), so the same permissions and confirmations apply.
Tools (43)
Tool names, descriptions and JSON schemas are written for models: each description says what the tool returns, which permission it needs and whether it spends money. Results are JSON (also returned as structuredContent). Grouped by area:
Getting started
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| get_started | org:read | readcard | Where am I? (setup status and next steps) |
get_started scope org:read read-onlycard: get-started
Where am I? (setup status and next steps)
Call this first in a new conversation. Returns the organization's setup state (plan, balance, agents, numbers, integrations, recent calls) and an ordered list of next steps, each naming the tool to call and the dashboard page to open. Nothing is changed.
Returns. { organization, plan, balance, agents, numbers, integrations, recentCalls, onboarding, nextSteps: [{ step, why, tool, dashboardUrl }], guidedAgentBuilderUrl }.
Example and input schema
{}
{
"type": "object",
"properties": {}
}
Agents and voices
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| list_agents | agents:read | read | List agents |
| get_agent | agents:read | readcard | Get an agent |
| create_agent | agents:write | writecard | Create an agent |
| update_agent | agents:write | writecard | Update an agent |
| publish_agent | agents:write | writecard | Publish an agent |
| test_agent | agents:read | read | Test an agent |
| list_voices | voices:read | read | List voices |
list_agents scope agents:read read-only
List agents
List the organization's voice agents with id, name, language, status, published version and voice tier. Use status: "live" to find agents that can take calls.
Returns. agents: array of { id, name, language, status, publishedVersion, voiceTier, firstMessage, createdAt }.
Example and input schema
{
"status": "live"
}
{
"type": "object",
"properties": {
"status": {
"default": "all",
"description": "Filter by lifecycle state. \`live\` = has a published version.",
"type": "string",
"enum": [
"all",
"live",
"draft",
"archived"
]
}
}
}
get_agent scope agents:read read-onlycard: agent
Get an agent
Full configuration of one agent (draft): instructions, opening line, voice, consent, transfer number, hours, analysis schema, tools and publish state.
Returns. The agent object with every editable field, the voice as a list_voices id, and publishedVersion (0 = never published).
Example and input schema
{
"agentId": "6ad0bf49171339b53c8e"
}
{
"type": "object",
"properties": {
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Agent id."
}
},
"required": [
"agentId"
]
}
create_agent scope agents:write writescard: agent
Create an agent
Create a draft voice agent from a template, overriding any field you pass (instructions, opening line, voice, hours, tools…). The configuration is validated with the voice platform before it is saved. The draft takes no calls until publish_agent. If the user would rather answer a few questions than review instructions, send them to the dashboard's guided builder (guidedAgentBuilderUrl from get_started, /dashboard/agents/new/guided): it writes the instructions, picks a voice and greeting and offers a browser test call.
Returns. The created agent (same shape as get_agent). Validation failures come back as an error listing the issues.
Example and input schema
{
"template": "receptionist",
"name": "Front desk",
"language": "en",
"instructions": "You are Mia, the receptionist of Northwind Clinic…"
}
{
"type": "object",
"properties": {
"template": {
"default": "receptionist",
"description": "Starting point for everything you do not set: instructions, opening line, consent text, summary prompt, analysis schema, built-in contact tools.",
"type": "string",
"enum": [
"receptionist",
"appointment_booking",
"lead_qualification",
"support"
]
},
"name": {
"description": "Display name of the agent, e.g. \"Front desk\".",
"type": "string",
"minLength": 1,
"maxLength": 80
},
"language": {
"description": "Language the agent speaks and understands: en or ar. Changes the default voice and consent text.",
"type": "string",
"enum": [
"en",
"ar"
]
},
"voiceId": {
"description": "Voice id from list_voices (not a provider voice id). Defaults to the language's default voice.",
"type": "string",
"maxLength": 64
},
"instructions": {
"description": "System instructions in plain language: who the agent is, what it may do and say, what it must never do. Markdown allowed.",
"type": "string",
"minLength": 1,
"maxLength": 20000
},
"firstMessage": {
"description": "The agent's opening line when a call connects.",
"type": "string",
"minLength": 1,
"maxLength": 600
},
"consentAnnouncement": {
"description": "Play a recording announcement at the start of every call (default true; keep it on unless local law allows otherwise).",
"type": "boolean"
},
"consentText": {
"description": "The announcement text, in the agent's language.",
"type": "string",
"maxLength": 300
},
"transferNumber": {
"description": "Human hand-off number for the transfer_call tool, or null to disable transfers.",
"anyOf": [
{
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$",
"description": "Phone number in E.164 format, e.g. +14155550123."
},
{
"type": "null"
}
]
},
"maxDurationSec": {
"description": "Hard cap on call length in seconds (default 900).",
"type": "integer",
"minimum": 30,
"maximum": 3600
},
"silenceTimeoutSec": {
"description": "Seconds of silence before the agent ends the call (default 20).",
"type": "integer",
"minimum": 5,
"maximum": 120
},
"endCallPhrases": {
"description": "Phrases that end the call when the caller says them.",
"maxItems": 20,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 80
}
},
"voicemailDetection": {
"description": "Hang up (outbound) when voicemail answers instead of talking to the machine (default true).",
"type": "boolean"
},
"memoryEnabled": {
"description": "Give the agent the contact's memory summary and let it update contact attributes (default true).",
"type": "boolean"
},
"summaryPrompt": {
"description": "Extra guidance for the post-call summary, or null for the default.",
"anyOf": [
{
"type": "string",
"maxLength": 2000
},
{
"type": "null"
}
]
},
"analysisSchema": {
"description": "JSON Schema (object) of the structured data to extract after each call, e.g. { type: \"object\", properties: { appointment_confirmed: { type: \"boolean\" } } }. Null disables extraction.",
"anyOf": [
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
{
"type": "null"
}
]
},
"businessHours": {
"description": "Opening hours the agent reads to callers; null means always open.",
"anyOf": [
{
"type": "object",
"properties": {
"timezone": {
"type": "string",
"maxLength": 64,
"description": "IANA time zone, e.g. Asia/Dubai."
},
"days": {
"maxItems": 7,
"type": "array",
"items": {
"type": "object",
"properties": {
"day": {
"type": "integer",
"minimum": 0,
"maximum": 6,
"description": "0 = Sunday … 6 = Saturday."
},
"open": {
"type": "string",
"pattern": "^\\d{2}:\\d{2}$",
"description": "HH:MM"
},
"close": {
"type": "string",
"pattern": "^\\d{2}:\\d{2}$",
"description": "HH:MM"
}
},
"required": [
"day",
"open",
"close"
]
}
},
"outsideHoursMessage": {
"description": "What the agent says outside these hours.",
"type": "string",
"maxLength": 500
}
},
"required": [
"timezone",
"days"
]
},
{
"type": "null"
}
]
},
"tools": {
"description": "Replaces the agent's tool list: built-in call tools and actions from connected integrations the agent may use during calls.",
"maxItems": 40,
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "builtin"
},
"name": {
"type": "string",
"enum": [
"transfer_call",
"end_call",
"update_contact",
"lookup_contact"
],
"description": "transfer_call (needs transferNumber), end_call, update_contact, lookup_contact."
}
},
"required": [
"kind",
"name"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "integration"
},
"slug": {
"type": "string",
"minLength": 1,
"maxLength": 120,
"description": "An action slug of a connected integration, from list_integrations (e.g. GOOGLECALENDAR_CREATE_EVENT)."
}
},
"required": [
"kind",
"slug"
]
}
]
}
}
}
}
update_agent scope agents:write writesidempotentcard: agent
Update an agent
Change fields of an agent's draft. Only the fields you pass change. Published calls keep using the last published version until you call publish_agent again.
Returns. The updated draft.
Example and input schema
{
"agentId": "6ad0bf49171339b53c8e",
"firstMessage": "Thanks for calling Northwind Clinic, this is Mia."
}
{
"type": "object",
"properties": {
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Agent id."
},
"name": {
"description": "Display name of the agent, e.g. \"Front desk\".",
"type": "string",
"minLength": 1,
"maxLength": 80
},
"language": {
"description": "Language the agent speaks and understands: en or ar. Changes the default voice and consent text.",
"type": "string",
"enum": [
"en",
"ar"
]
},
"voiceId": {
"description": "Voice id from list_voices (not a provider voice id). Defaults to the language's default voice.",
"type": "string",
"maxLength": 64
},
"instructions": {
"description": "System instructions in plain language: who the agent is, what it may do and say, what it must never do. Markdown allowed.",
"type": "string",
"minLength": 1,
"maxLength": 20000
},
"firstMessage": {
"description": "The agent's opening line when a call connects.",
"type": "string",
"minLength": 1,
"maxLength": 600
},
"consentAnnouncement": {
"description": "Play a recording announcement at the start of every call (default true; keep it on unless local law allows otherwise).",
"type": "boolean"
},
"consentText": {
"description": "The announcement text, in the agent's language.",
"type": "string",
"maxLength": 300
},
"transferNumber": {
"description": "Human hand-off number for the transfer_call tool, or null to disable transfers.",
"anyOf": [
{
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$",
"description": "Phone number in E.164 format, e.g. +14155550123."
},
{
"type": "null"
}
]
},
"maxDurationSec": {
"description": "Hard cap on call length in seconds (default 900).",
"type": "integer",
"minimum": 30,
"maximum": 3600
},
"silenceTimeoutSec": {
"description": "Seconds of silence before the agent ends the call (default 20).",
"type": "integer",
"minimum": 5,
"maximum": 120
},
"endCallPhrases": {
"description": "Phrases that end the call when the caller says them.",
"maxItems": 20,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 80
}
},
"voicemailDetection": {
"description": "Hang up (outbound) when voicemail answers instead of talking to the machine (default true).",
"type": "boolean"
},
"memoryEnabled": {
"description": "Give the agent the contact's memory summary and let it update contact attributes (default true).",
"type": "boolean"
},
"summaryPrompt": {
"description": "Extra guidance for the post-call summary, or null for the default.",
"anyOf": [
{
"type": "string",
"maxLength": 2000
},
{
"type": "null"
}
]
},
"analysisSchema": {
"description": "JSON Schema (object) of the structured data to extract after each call, e.g. { type: \"object\", properties: { appointment_confirmed: { type: \"boolean\" } } }. Null disables extraction.",
"anyOf": [
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
{
"type": "null"
}
]
},
"businessHours": {
"description": "Opening hours the agent reads to callers; null means always open.",
"anyOf": [
{
"type": "object",
"properties": {
"timezone": {
"type": "string",
"maxLength": 64,
"description": "IANA time zone, e.g. Asia/Dubai."
},
"days": {
"maxItems": 7,
"type": "array",
"items": {
"type": "object",
"properties": {
"day": {
"type": "integer",
"minimum": 0,
"maximum": 6,
"description": "0 = Sunday … 6 = Saturday."
},
"open": {
"type": "string",
"pattern": "^\\d{2}:\\d{2}$",
"description": "HH:MM"
},
"close": {
"type": "string",
"pattern": "^\\d{2}:\\d{2}$",
"description": "HH:MM"
}
},
"required": [
"day",
"open",
"close"
]
}
},
"outsideHoursMessage": {
"description": "What the agent says outside these hours.",
"type": "string",
"maxLength": 500
}
},
"required": [
"timezone",
"days"
]
},
{
"type": "null"
}
]
},
"tools": {
"description": "Replaces the agent's tool list: built-in call tools and actions from connected integrations the agent may use during calls.",
"maxItems": 40,
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "builtin"
},
"name": {
"type": "string",
"enum": [
"transfer_call",
"end_call",
"update_contact",
"lookup_contact"
],
"description": "transfer_call (needs transferNumber), end_call, update_contact, lookup_contact."
}
},
"required": [
"kind",
"name"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "integration"
},
"slug": {
"type": "string",
"minLength": 1,
"maxLength": 120,
"description": "An action slug of a connected integration, from list_integrations (e.g. GOOGLECALENDAR_CREATE_EVENT)."
}
},
"required": [
"kind",
"slug"
]
}
]
}
}
},
"required": [
"agentId"
]
}
publish_agent scope agents:write writescard: agent
Publish an agent
Validate the draft (locally and with the voice platform) and publish it as a new immutable version. From this moment inbound calls on assigned numbers and start_call use the new version.
Returns. { agentId, version, status: "live" }, or an error listing blocking issues.
Example and input schema
{
"agentId": "6ad0bf49171339b53c8e"
}
{
"type": "object",
"properties": {
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Agent id."
}
},
"required": [
"agentId"
]
}
test_agent scope agents:read read-only
Test an agent
Dry-run check of an agent: validates the draft with the voice platform and reports warnings, then tells you how to hear it: a link to the in-browser test call (free trial minutes, no phone needed) and, when the organization has an outbound number, how to call the user's own phone with start_call.
Returns. { agentId, ok, issues, browserTestUrl, phoneTest: { possible, phoneNumberId, note } }.
Example and input schema
{
"agentId": "6ad0bf49171339b53c8e"
}
{
"type": "object",
"properties": {
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Agent id."
}
},
"required": [
"agentId"
]
}
list_voices scope voices:read read-only
List voices
Browse the curated English and Arabic voice catalog (accents, gender, standard/premium tier, preview audio). Pass the returned id as voiceId to create_agent / update_agent.
Returns. voices: array of { id, name, language, gender, accent, tier, description, previewUrl }.
Example and input schema
{
"language": "ar",
"gender": "female"
}
{
"type": "object",
"properties": {
"language": {
"description": "Only voices for this language.",
"type": "string",
"enum": [
"en",
"ar"
]
},
"gender": {
"type": "string",
"enum": [
"male",
"female"
]
},
"tier": {
"description": "Premium voices cost more per minute (see get_usage for the plan's rates).",
"type": "string",
"enum": [
"standard",
"premium"
]
},
"accent": {
"description": "Substring match on the accent label, e.g. \"gulf\", \"egyptian\", \"british\".",
"type": "string",
"maxLength": 40
}
}
}
Phone numbers
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| list_number_countries | numbers:read | read | Countries where numbers are offered |
| list_numbers | numbers:read | readcard | List phone numbers |
| get_number | numbers:read | readcard | Get a phone number |
| search_numbers | numbers:read | readcard | Search available numbers |
| buy_number | numbers:write | writemoneycard | Buy a phone number |
| request_number | numbers:write | writemoney | Request a number that needs registration |
| assign_number | numbers:write | writecard | Assign a number to an agent |
| release_number | numbers:write | writeirreversible | Release a phone number |
list_number_countries scope numbers:read read-only
Countries where numbers are offered
The number catalogue: every country with the number types offered, whether each is bought instantly (search_numbers + buy_number) or on request (request_number, carrier registration handled by our team), the monthly price for this organization, inbound-only types and the documents a request needs.
Returns. countries: array of { code, name, dialCode, offers: [{ type, mode, monthlyPriceCents, inboundOnly, documents }] }, plus comingSoon.
Example and input schema
{
"mode": "instant"
}
{
"type": "object",
"properties": {
"country": {
"description": "One country only (ISO 3166-1 alpha-2), e.g. AE.",
"type": "string",
"minLength": 2,
"maxLength": 2
},
"mode": {
"default": "all",
"description": "\`instant\` = buy self-serve now; \`request\` = needs carrier registration (request_number).",
"type": "string",
"enum": [
"all",
"instant",
"request"
]
}
}
}
list_numbers scope numbers:read read-onlycard: numbers
List phone numbers
The organization's phone numbers with status, assigned agent and whether they can dial out. Use an active number with inboundOnly: false as phoneNumberId for start_call.
Returns. numbers: array of { id, phone, country, type, status, agentId, inboundOnly, monthlyPriceCents, createdAt }, plus requests (open number requests).
Example and input schema
{}
{
"type": "object",
"properties": {}
}
get_number scope numbers:read read-onlycard: numbers
Get a phone number
One number with its status, the agent answering it, pricing and the last provisioning error if any.
Returns. The number object.
Example and input schema
{
"phoneNumberId": "68ee11aa00c1d2e3f405"
}
{
"type": "object",
"properties": {
"phoneNumberId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Phone number id."
}
},
"required": [
"phoneNumberId"
]
}
search_numbers scope numbers:read read-onlyexternal effectcard: number-search
Search available numbers
Search the carrier inventory for numbers you could buy now, with the monthly price for this organization. Countries that need registration return no inventory and point to request_number instead. Nothing is purchased.
Returns. numbers: candidates { phone, locality, region }, plus monthlyPriceCents, includedInPlan, inboundOnly, regulatory (none | documents) and eligibility.
Example and input schema
{
"country": "US",
"type": "local",
"areaCode": "415"
}
{
"type": "object",
"properties": {
"country": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "ISO 3166-1 alpha-2 country code; list_number_countries shows what is offered."
},
"type": {
"default": "local",
"description": "local, tollfree or mobile. Not every type exists in every country; the result says which.",
"type": "string",
"enum": [
"local",
"tollfree",
"mobile"
]
},
"contains": {
"description": "Digits the number should contain, e.g. \"555\" or a vanity pattern.",
"type": "string",
"maxLength": 12
},
"areaCode": {
"description": "Area code (US and CA only).",
"type": "string",
"maxLength": 5
},
"limit": {
"default": 10,
"type": "integer",
"minimum": 1,
"maximum": 30
}
},
"required": [
"country"
]
}
buy_number scope numbers:write writesexternal effectcosts money · confirm: truecard: number-search
Buy a phone number
Buy a number from search_numbers for this organization (monthly charge on the subscription unless the plan includes it). Requires confirm: true. Optionally assigns a published agent at once. Countries that need registration are not bought here: use request_number.
Returns. { purchased: true, number } when the number is active, or { purchased: false, reason, requirements?, dashboardUrl? }.
Example and input schema
{
"phone": "+14155550123",
"country": "US",
"type": "local",
"confirm": true
}
{
"type": "object",
"properties": {
"phone": {
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$",
"description": "A number returned by search_numbers, in E.164."
},
"country": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "Country code used in the search."
},
"type": {
"default": "local",
"type": "string",
"enum": [
"local",
"tollfree",
"mobile"
]
},
"agentId": {
"description": "Assign this published agent to answer the number right after purchase.",
"type": "string",
"minLength": 1,
"maxLength": 64
},
"confirm": {
"default": false,
"description": "Must be true. Buying a number starts a monthly charge. Ask the user before setting this.",
"type": "boolean"
}
},
"required": [
"phone",
"country"
]
}
request_number scope numbers:write writesexternal effectcosts money · confirm: true
Request a number that needs registration
For countries whose offer is request (United Kingdom, UAE, Saudi Arabia, Germany, …): files a number request with the business details; our team completes the carrier registration and activates the number (usually within three business days). Supporting documents are uploaded by the user in the dashboard (the result links to the form). Requires confirm: true.
Returns. { requestId, status: "requested", dashboardUrl, note }.
Example and input schema
{
"country": "GB",
"type": "local",
"fields": {
"businessName": "Northwind Ltd",
"address": {
"street": "1 High Street",
"city": "Reading",
"postalCode": "RG1 1AA",
"country": "GB"
},
"contactName": "Sara Ahmed",
"contactEmail": "sara@northwind.example",
"intendedUse": "Customer support line"
},
"confirm": true
}
{
"type": "object",
"properties": {
"country": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "A country whose offer is \`request\` in list_number_countries (e.g. GB, AE, SA, DE)."
},
"type": {
"default": "local",
"type": "string",
"enum": [
"local",
"tollfree",
"mobile"
]
},
"quantity": {
"default": 1,
"type": "integer",
"minimum": 1,
"maximum": 20
},
"fields": {
"type": "object",
"properties": {
"businessName": {
"type": "string",
"minLength": 2,
"maxLength": 200
},
"registrationNumber": {
"description": "Trade licence or company registration number, when the country asks for it.",
"type": "string",
"maxLength": 120
},
"address": {
"type": "object",
"properties": {
"street": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"street2": {
"type": "string",
"maxLength": 200
},
"city": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"region": {
"type": "string",
"maxLength": 120
},
"postalCode": {
"type": "string",
"maxLength": 32
},
"country": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "ISO 3166-1 alpha-2 of the registered address (usually the number's country)."
}
},
"required": [
"street",
"city",
"country"
]
},
"contactName": {
"type": "string",
"minLength": 2,
"maxLength": 120
},
"contactEmail": {
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
"contactPhone": {
"type": "string",
"maxLength": 32
},
"intendedUse": {
"type": "string",
"minLength": 5,
"maxLength": 1000,
"description": "What the number is for, e.g. \"customer support line for a dental clinic\"."
},
"notes": {
"type": "string",
"maxLength": 1000
}
},
"required": [
"businessName",
"address",
"contactName",
"contactEmail",
"intendedUse"
],
"description": "Business details the carrier registration needs. Documents (licence, ID, proof of address) are uploaded later in the dashboard; the result links to the form."
},
"confirm": {
"default": false,
"description": "Must be true. A request starts a registration our team completes and ends with a monthly charge once the number is activated. Ask the user before setting this.",
"type": "boolean"
}
},
"required": [
"country",
"fields"
]
}
assign_number scope numbers:write writesidempotentcard: numbers
Assign a number to an agent
Set (or clear with null) the agent that answers inbound calls on a number. The agent must be published to actually answer.
Returns. The updated number.
Example and input schema
{
"phoneNumberId": "68ee11aa00c1d2e3f405",
"agentId": "6ad0bf49171339b53c8e"
}
{
"type": "object",
"properties": {
"phoneNumberId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Phone number id."
},
"agentId": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Agent id."
},
{
"type": "null"
}
],
"description": "Agent that answers inbound calls on this number, or null to unassign."
}
},
"required": [
"phoneNumberId",
"agentId"
]
}
release_number scope numbers:write writesexternal effectirreversible · confirm: true
Release a phone number
Give a number back to the carrier and stop its monthly charge. Irreversible: callers get a disconnected tone and the number cannot be bought again for 30 days. Requires confirm: true.
Returns. { released: true, phone }.
Example and input schema
{
"phoneNumberId": "68ee11aa00c1d2e3f405",
"confirm": true
}
{
"type": "object",
"properties": {
"phoneNumberId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Phone number id."
},
"confirm": {
"default": false,
"description": "Must be true. Releasing a number is irreversible: callers can no longer reach it and the number cannot be bought back for 30 days. Ask the user before setting this.",
"type": "boolean"
}
},
"required": [
"phoneNumberId"
]
}
Calls (inbound and outbound)
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| start_call | calls:write | writemoneycard | Start an outbound call |
| end_call | calls:write | writecard | End a live call |
| get_call | calls:read | readcard | Get a call |
| get_transcript | calls:read | read | Get a transcript |
| get_recording_url | calls:read | read | Get a recording link |
| list_calls | calls:read | readcard | List calls |
| list_live_calls | calls:read | readcard | List live calls |
start_call scope calls:write writesexternal effectcosts money · confirm: truecard: call
Start an outbound call
Dial a phone number with a published agent from one of the organization's numbers. note tells the agent what this call is about. Returns immediately with the call id while the phone rings; the conversation, summary and outcome arrive asynchronously (poll get_call, or receive the call.summary.ready webhook). Requires confirm: true.
Returns. { callId, status: "queued", note }. Errors: agent_not_published, number_not_active, number_inbound_only, payment_required, concurrency_limit, contact_do_not_call, country_not_allowed, call_start_failed.
Example and input schema
{
"agentId": "6ad0bf49171339b53c8e",
"to": "+14155550123",
"contactName": "Sara Ahmed",
"note": "Confirm Thursday's 10:00 appointment",
"idempotencyKey": "order-8841-confirm",
"confirm": true
}
{
"type": "object",
"properties": {
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "A published agent (publishedVersion > 0)."
},
"to": {
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$",
"description": "Number to dial."
},
"phoneNumberId": {
"description": "Your caller-id number. Optional when the organization has exactly one active outbound number.",
"type": "string",
"minLength": 1,
"maxLength": 64
},
"contactName": {
"description": "Name passed to the agent when the number is not a known contact.",
"type": "string",
"maxLength": 120
},
"note": {
"description": "Context for this call only, exposed to the agent as {{call_note}}, e.g. \"Confirm Thursday 10:00; offer Friday if not possible\".",
"type": "string",
"maxLength": 1000
},
"variables": {
"description": "Extra prompt variables for this call, available as {{campaign.<key>}}.",
"type": "object",
"propertyNames": {
"type": "string",
"maxLength": 64
},
"additionalProperties": {
"type": "string",
"maxLength": 500
}
},
"idempotencyKey": {
"description": "Unique per intended call (e.g. an order id). Repeating a call with the same key returns the first call instead of dialing again.",
"type": "string",
"minLength": 1,
"maxLength": 128
},
"confirm": {
"default": false,
"description": "Must be true. A call costs money and rings a real phone. Ask the user before setting this.",
"type": "boolean"
}
},
"required": [
"agentId",
"to"
]
}
end_call scope calls:write writesidempotentexternal effectcard: call
End a live call
Hang up a call that is queued, ringing or in progress (for example one started with start_call that should not continue). Requires confirm: true. Idempotent: a call that already ended is reported as such and nothing changes. The transcript, summary and cost of the ended part still arrive asynchronously (get_call).
Returns. { callId, status, alreadyEnded, endedReason, note }. Errors: not_found (unknown id or another organization's call), call_not_ready (still being set up; retry in a few seconds), call_end_failed.
Example and input schema
{
"callId": "68ef3b2c001a9d4e7f10",
"confirm": true
}
{
"type": "object",
"properties": {
"callId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "A call that is queued, ringing or in progress (from start_call or list_calls)."
},
"confirm": {
"default": false,
"description": "Must be true. Ending a call hangs up on a real person. Ask the user before setting this.",
"type": "boolean"
}
},
"required": [
"callId"
]
}
get_call scope calls:read read-onlycard: call
Get a call
Status and results of a call: direction, numbers, timing, cost, summary, outcome, sentiment, structured data and the actions the agent took (plain-language labels). While the call is live this is the status to poll. The transcript is a separate tool (get_transcript).
Returns. The call object; summary / outcome are null until post-call analysis finishes (usually within a minute of hang-up). endedReason is a plain-language label.
Example and input schema
{
"callId": "68ef3b2c001a9d4e7f10"
}
{
"type": "object",
"properties": {
"callId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Call id."
}
},
"required": [
"callId"
]
}
get_transcript scope calls:read read-only
Get a transcript
The conversation of a call, as text or as structured turns. Grows while the call is in progress (live transcript).
Returns. { callId, status, turns } (json) or { callId, status, text }.
Example and input schema
{
"callId": "68ef3b2c001a9d4e7f10",
"format": "text"
}
{
"type": "object",
"properties": {
"callId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Call id."
},
"format": {
"default": "text",
"description": "\`text\` = one line per turn (\"agent: …\"), \`json\` = array of { role, text, at }.",
"type": "string",
"enum": [
"text",
"json"
]
}
},
"required": [
"callId"
]
}
get_recording_url scope calls:read read-only
Get a recording link
A signed, short-lived (10 minutes) URL to the call's audio recording. Fetch it again when it expires. Null when the call has no recording (in progress, failed, or deleted by retention).
Returns. { callId, url, expiresAt } or { callId, url: null, reason }.
Example and input schema
{
"callId": "68ef3b2c001a9d4e7f10"
}
{
"type": "object",
"properties": {
"callId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Call id."
}
},
"required": [
"callId"
]
}
list_calls scope calls:read read-onlycard: calls
List calls
Calls newest first, with optional filters by direction (inbound / outbound), agent, status, time window, contact or campaign. Paginate with cursor. Each row carries summary, outcome and sentiment once analysed.
Returns. { calls: Call[], nextCursor }.
Example and input schema
{
"direction": "inbound",
"status": "ended",
"since": "2026-10-01T00:00:00Z",
"limit": 20
}
{
"type": "object",
"properties": {
"direction": {
"description": "Inbound = callers dialled your number; outbound = start_call or campaigns; web = browser test calls.",
"type": "string",
"enum": [
"inbound",
"outbound",
"web"
]
},
"agentId": {
"description": "Only calls handled by this agent.",
"type": "string",
"minLength": 1,
"maxLength": 64
},
"status": {
"type": "string",
"enum": [
"queued",
"ringing",
"in_progress",
"ended",
"failed",
"no_answer",
"busy",
"voicemail"
]
},
"since": {
"description": "Started at or after this instant.",
"type": "string",
"maxLength": 40
},
"until": {
"description": "Started at or before this instant.",
"type": "string",
"maxLength": 40
},
"contact": {
"description": "A contact id or an E.164 phone number.",
"type": "string",
"maxLength": 64
},
"campaignId": {
"description": "Only calls placed by this campaign.",
"type": "string",
"minLength": 1,
"maxLength": 64
},
"limit": {
"default": 25,
"description": "Page size, 1–100.",
"type": "integer",
"minimum": 1,
"maximum": 100
},
"cursor": {
"description": "Opaque \`nextCursor\` from the previous page. Omit for the first page.",
"type": "string",
"maxLength": 200
}
}
}
list_live_calls scope calls:read read-onlycard: calls
List live calls
Calls that are queued, ringing or in progress right now, with how long each has been running and the line usage against the concurrency limit.
Returns. { calls: [{ id, direction, from, to, status, agentId, startedAt, elapsedSec }], inProgress, concurrencyLimit }.
Example and input schema
{}
{
"type": "object",
"properties": {}
}
Outbound campaigns
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| list_campaigns | campaigns:read | read | List campaigns |
| get_campaign | campaigns:read | readcard | Get a campaign |
| get_campaign_results | campaigns:read | read | Get campaign results |
| create_campaign | campaigns:write | writemoneycard | Create a campaign |
| set_campaign_status | campaigns:write | writemoneycard | Start, pause, resume or cancel a campaign |
list_campaigns scope campaigns:read read-only
List campaigns
Outbound campaigns with status and progress counters (queued, calling, done, failed, skipped).
Returns. campaigns: array of { id, name, status, agentId, phoneNumberId, total, queued, calling, done, failed, skipped, startAt, startedAt, completedAt }.
Example and input schema
{
"status": "running"
}
{
"type": "object",
"properties": {
"status": {
"default": "all",
"type": "string",
"enum": [
"all",
"draft",
"scheduled",
"running",
"paused",
"completed",
"cancelled"
]
}
}
}
get_campaign scope campaigns:read read-onlycard: campaign
Get a campaign
One campaign with its settings (window, pacing, retries) and live progress. Poll it while running.
Returns. The campaign object plus progressPct and outcomes (reached / no answer / busy / voicemail / failed counts).
Example and input schema
{
"campaignId": "6ad129fa2c626a8bfa43"
}
{
"type": "object",
"properties": {
"campaignId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Campaign id."
}
},
"required": [
"campaignId"
]
}
get_campaign_results scope campaigns:read read-only
Get campaign results
Per-contact rows of a campaign: state, attempts, last call id and outcome, in file order. Paginate with cursor; use get_call / get_transcript for details of a row's call.
Returns. { rows: [{ id, phone, name, status, reason, attempts, lastCallId, lastCallStatus, outcome, nextAttemptAt }], nextCursor }.
Example and input schema
{
"campaignId": "6ad129fa2c626a8bfa43",
"status": "done",
"limit": 50
}
{
"type": "object",
"properties": {
"campaignId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Campaign id."
},
"status": {
"description": "Only rows in this state.",
"type": "string",
"enum": [
"queued",
"calling",
"done",
"failed",
"skipped"
]
},
"limit": {
"default": 25,
"description": "Page size, 1–100.",
"type": "integer",
"minimum": 1,
"maximum": 100
},
"cursor": {
"description": "Opaque \`nextCursor\` from the previous page. Omit for the first page.",
"type": "string",
"maxLength": 200
}
},
"required": [
"campaignId"
]
}
create_campaign scope campaigns:write writesexternal effectcosts money · confirm: truecard: campaign
Create a campaign
Create an outbound campaign from a contact list or CSV text. Rows with invalid phones, duplicates, do-not-call contacts or countries the organization may not dial are rejected and reported. start: "draft" only saves; now / at dial real phones and need confirm: true and the organization's outbound compliance acknowledgement (done once in the dashboard).
Returns. { campaignId, status, accepted, rejected: [{ line, phone, reason }], note }.
Example and input schema
{
"name": "October renewals",
"agentId": "6ad0bf49171339b53c8e",
"phoneNumberId": "68ee11aa00c1d2e3f405",
"contacts": [
{
"phone": "+14155550123",
"name": "Sara Ahmed",
"variables": {
"plan": "Pro"
}
}
],
"start": "draft"
}
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 120
},
"agentId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "A published agent."
},
"phoneNumberId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Caller-id number (active, outbound-capable)."
},
"contacts": {
"description": "The call list. Use this or \`csv\`.",
"maxItems": 2000,
"type": "array",
"items": {
"type": "object",
"properties": {
"phone": {
"type": "string",
"minLength": 3,
"maxLength": 32,
"description": "E.164 preferred; national formats are normalised with the caller-id country."
},
"name": {
"type": "string",
"maxLength": 120
},
"variables": {
"description": "Per-contact prompt variables ({{campaign.<key>}}), e.g. { order: \"8841\" }.",
"type": "object",
"propertyNames": {
"type": "string",
"maxLength": 64
},
"additionalProperties": {
"type": "string",
"maxLength": 500
}
}
},
"required": [
"phone"
]
}
},
"csv": {
"description": "Raw CSV text with a header row (phone or number column required; name and any other columns become variables).",
"type": "string",
"maxLength": 2000000
},
"start": {
"default": "draft",
"description": "\`draft\` saves without dialling; \`now\` starts immediately; \`at\` schedules \`startAt\`.",
"type": "string",
"enum": [
"draft",
"now",
"at"
]
},
"startAt": {
"description": "Required when start = at.",
"type": "string",
"maxLength": 40
},
"timezone": {
"description": "Organization time zone for \`startAt\` (default: the organization's).",
"type": "string",
"maxLength": 64
},
"window": {
"description": "Calling hours, applied in each destination's local time (default: the organization's outbound window).",
"type": "object",
"properties": {
"start": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
"description": "HH:MM local time in the destination's time zone."
},
"end": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
"description": "HH:MM."
},
"days": {
"minItems": 1,
"maxItems": 7,
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 6
},
"description": "0 = Sunday … 6 = Saturday."
}
},
"required": [
"start",
"end",
"days"
]
},
"maxConcurrent": {
"default": 2,
"description": "Lines this campaign may use at once (one line is always kept free for inbound).",
"type": "integer",
"minimum": 1,
"maximum": 50
},
"maxAttempts": {
"default": 2,
"description": "Attempts per contact when nobody answers or the line is busy.",
"type": "integer",
"minimum": 1,
"maximum": 5
},
"retryDelayMin": {
"default": 120,
"description": "Minutes between attempts.",
"type": "integer",
"minimum": 5,
"maximum": 1440
},
"voicemailDetection": {
"default": true,
"type": "boolean"
},
"confirm": {
"default": false,
"description": "Required (true) when start is \`now\` or \`at\`: a campaign dials real phones and spends money. Drafts need no confirmation.",
"type": "boolean"
}
},
"required": [
"name",
"agentId",
"phoneNumberId"
]
}
set_campaign_status scope campaigns:write writesexternal effectcosts money · confirm: truecard: campaign
Start, pause, resume or cancel a campaign
Move a campaign: draft/paused → running (dials phones, needs confirm), running → paused, any open state → cancelled (irreversible, needs confirm).
Returns. The campaign with its new status.
Example and input schema
{
"campaignId": "6ad129fa2c626a8bfa43",
"status": "running",
"confirm": true
}
{
"type": "object",
"properties": {
"campaignId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Campaign id."
},
"status": {
"type": "string",
"enum": [
"running",
"paused",
"cancelled"
],
"description": "running = start or resume; paused = stop dialling, keep the queue; cancelled = stop for good."
},
"confirm": {
"default": false,
"description": "Required (true) for \`running\` (dials phones, spends money) and \`cancelled\` (irreversible). Pausing needs no confirmation.",
"type": "boolean"
}
},
"required": [
"campaignId",
"status"
]
}
Contacts
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| list_contacts | contacts:read | readcard | List contacts |
| get_contact | contacts:read | readcard | Get a contact |
| upsert_contact | contacts:write | writecard | Create or update a contact |
| set_do_not_call | contacts:write | writecard | Set do-not-call |
list_contacts scope contacts:read read-onlycard: contacts
List contacts
Contacts newest first, filtered by free-text search (name, phone prefix, email), exact phone, tag or do-not-call flag. Paginate with cursor.
Returns. { contacts: Contact[], nextCursor }.
Example and input schema
{
"search": "sara",
"limit": 20
}
{
"type": "object",
"properties": {
"search": {
"description": "Name, phone prefix or email.",
"type": "string",
"maxLength": 120
},
"phone": {
"description": "Exact match (at most one result).",
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$"
},
"tag": {
"description": "Contacts carrying this tag.",
"type": "string",
"maxLength": 64
},
"doNotCall": {
"type": "boolean"
},
"limit": {
"default": 25,
"description": "Page size, 1–100.",
"type": "integer",
"minimum": 1,
"maximum": 100
},
"cursor": {
"description": "Opaque \`nextCursor\` from the previous page. Omit for the first page.",
"type": "string",
"maxLength": 200
}
}
}
get_contact scope contacts:read read-onlycard: contacts
Get a contact
One contact by id or phone, including the memory summary the agent is given at the start of each call, attributes, tags, notes and call count.
Returns. The contact, or an error not_found.
Example and input schema
{
"phone": "+14155550123"
}
{
"type": "object",
"properties": {
"contactId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Contact id."
},
"phone": {
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$",
"description": "Phone number in E.164 format, e.g. +14155550123."
}
}
}
upsert_contact scope contacts:write writesidempotentcard: contacts
Create or update a contact
Create the contact for a phone number or update it when it exists. Attributes merge; tags replace.
Returns. { created: boolean, contact }.
Example and input schema
{
"phone": "+14155550123",
"name": "Sara Ahmed",
"tags": [
"vip"
],
"attributes": {
"plan": "pro"
}
}
{
"type": "object",
"properties": {
"phone": {
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$",
"description": "Identifies the contact: created when unknown, updated otherwise."
},
"name": {
"anyOf": [
{
"type": "string",
"maxLength": 120
},
{
"type": "null"
}
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 320,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"attributes": {
"description": "Key/value strings merged into the stored attributes (set a key to \"\" to clear it). The agent can read and update them during calls.",
"type": "object",
"propertyNames": {
"type": "string",
"maxLength": 64
},
"additionalProperties": {
"type": "string",
"maxLength": 500
}
},
"tags": {
"description": "Replaces the tag list.",
"maxItems": 50,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 64
}
},
"notes": {
"description": "Internal notes (not spoken by the agent).",
"type": "string",
"maxLength": 4000
},
"doNotCall": {
"type": "boolean"
}
},
"required": [
"phone"
]
}
set_do_not_call scope contacts:write writesidempotentcard: contacts
Set do-not-call
Block (or unblock) outbound calls to a contact. start_call and campaigns refuse blocked contacts.
Returns. The contact with the new flag.
Example and input schema
{
"phone": "+14155550123",
"doNotCall": true
}
{
"type": "object",
"properties": {
"contactId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Contact id."
},
"phone": {
"description": "Creates the contact when it does not exist yet, so the flag sticks.",
"type": "string",
"pattern": "^\\+[1-9]\\d{6,14}$"
},
"doNotCall": {
"default": true,
"description": "true blocks outbound calls to this contact; false lifts the block.",
"type": "boolean"
}
}
}
Balance and billing
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| get_usage | usage:read | readcard | Balance, plan and usage |
| list_invoices | billing:read | read | List invoices |
| get_topup_link | billing:read | readcard | Top-up link |
| get_plan_link | billing:read | readcard | Plan change link |
get_usage scope usage:read read-onlycard: balance
Balance, plan and usage
Plan, included minutes left this period, prepaid wallet balance, number allowance, concurrency limit, per-minute overage rates and what this period cost so far. Check it before placing many calls or buying numbers.
Returns. { organization, plan, period, minutes, wallet, numbers, concurrency, rates, periodSpend }.
Example and input schema
{}
{
"type": "object",
"properties": {}
}
list_invoices scope billing:read read-onlyexternal effect
List invoices
Recent invoices of the subscription (amount, status, period, hosted PDF link).
Returns. invoices: array of { id, number, status, amountCents, currency, createdAt, periodStart, periodEnd, url }.
Example and input schema
{
"limit": 6
}
{
"type": "object",
"properties": {
"limit": {
"default": 12,
"type": "integer",
"minimum": 1,
"maximum": 50
}
}
}
get_topup_link scope billing:read read-onlyexternal effectcard: balance
Top-up link
A checkout link that adds a prepaid pack to the wallet. Nothing is charged by this tool: the user pays on the hosted checkout page. Requires an active subscription.
Returns. { url, amountCents, expiresNote }.
Example and input schema
{
"amountCents": 5000
}
{
"type": "object",
"properties": {
"amountCents": {
"default": 5000,
"description": "Prepaid pack in cents: 2500, 5000, 10000 or 25000 ($25, $50, $100, $250).",
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
}
}
get_plan_link scope billing:read read-onlyexternal effectcard: balance
Plan change link
A link to pick a plan (trial organizations: checkout for planId) or to change / cancel the plan and update the card (subscribed organizations: the billing portal). Nothing is charged by this tool.
Returns. { url, kind: "checkout" | "portal" }.
Example and input schema
{
"planId": "growth"
}
{
"type": "object",
"properties": {
"planId": {
"description": "Plan to subscribe to (trial organizations). Subscribed organizations get a billing-portal link to change plans instead; omit planId for the portal.",
"type": "string",
"enum": [
"starter",
"growth",
"scale"
]
}
}
}
Integrations
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| list_integrations | integrations:read | readcard | List integrations |
| get_integration_connect_link | integrations:read | readcard | Connect-a-tool link |
list_integrations scope integrations:read read-onlycard: integrations
List integrations
The verified tool catalogue (calendars, CRMs, helpdesks, spreadsheets, messaging) with each tool's connection status for this organization and the action slugs an agent can use during calls (pass them to update_agent tools).
Returns. integrations: array of { toolkit, name, category, status, connectedBy, actions: [{ slug, title, midCall }] }, plus connectedCount.
Example and input schema
{
"connectedOnly": true
}
{
"type": "object",
"properties": {
"connectedOnly": {
"default": false,
"description": "Only tools the organization has connected.",
"type": "boolean"
}
}
}
get_integration_connect_link scope integrations:read read-onlyexternal effectcard: integrations
Connect-a-tool link
A link the user opens (signed in to the dashboard) to connect a tool with their own account; the sign-in happens on the tool's own site.
Returns. { url, toolkit, note }.
Example and input schema
{
"toolkit": "googlecalendar"
}
{
"type": "object",
"properties": {
"toolkit": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Toolkit slug from list_integrations, e.g. googlecalendar, hubspot, slack."
}
},
"required": [
"toolkit"
]
}
Webhooks
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| list_webhooks | webhooks:read | readcard | List webhooks |
| create_webhook | webhooks:write | writecard | Create a webhook |
| test_webhook | webhooks:write | writecard | Send a test event |
list_webhooks scope webhooks:read read-onlycard: webhooks
List webhooks
Registered webhook endpoints with their events, active flag and the last deliveries (status, attempts).
Returns. webhooks: array of { id, url, events, active, recentDeliveries }.
Example and input schema
{}
{
"type": "object",
"properties": {}
}
create_webhook scope webhooks:write writescard: webhooks
Create a webhook
Register an HTTPS endpoint that receives signed events (call.started, call.ended, call.summary.ready, number.activated, wallet.low). The signing secret is returned once; store it to verify the X-AgentCalls-Signature header.
Returns. { id, url, events, secret } — the secret is never shown again.
Example and input schema
{
"url": "https://example.com/agentcalls",
"events": [
"call.summary.ready"
]
}
{
"type": "object",
"properties": {
"url": {
"type": "string",
"maxLength": 1024,
"format": "uri",
"description": "HTTPS endpoint that receives signed JSON events."
},
"events": {
"minItems": 1,
"type": "array",
"items": {
"type": "string",
"enum": [
"call.started",
"call.ended",
"call.summary.ready",
"number.activated",
"wallet.low"
]
},
"description": "Events to deliver."
}
},
"required": [
"url",
"events"
]
}
test_webhook scope webhooks:write writesidempotentexternal effectcard: webhooks
Send a test event
POST a signed sample call.ended event to the endpoint and report the HTTP status and latency. Nothing is queued or retried.
Returns. { ok, status, durationMs, error? }.
Example and input schema
{
"webhookId": "68ef3c10000a1b2c3d4e"
}
{
"type": "object",
"properties": {
"webhookId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Webhook id."
}
},
"required": [
"webhookId"
]
}
Organization
| Tool | Scope | Kind | Summary |
|---|---|---|---|
| get_organization | org:read | readcard | Get organization settings |
| update_outbound_settings | org:write | writecard | Update outbound calling rules |
get_organization scope org:read read-onlycard: organization
Get organization settings
Name, language, status, plan, home country, concurrency limit and the outbound calling rules (allowed countries, calling hours, time zone, consent text, compliance acknowledgement).
Returns. { id, name, logoUrl, locale, status, planId, homeCountry, concurrencyLimit, outbound: { allowedCountries, defaultCountries, window, timezone, consentText, acknowledged } }. logoUrl is the workspace logo (null = none set).
Example and input schema
{}
{
"type": "object",
"properties": {}
}
update_outbound_settings scope org:write writesidempotentcard: organization
Update outbound calling rules
Change which countries outbound calls may dial, the default calling hours and time zone, and the outbound consent text. The compliance acknowledgement (TCPA / do-not-call responsibility) cannot be given here: a person must tick it in the dashboard before campaigns can start.
Returns. The outbound settings after the change.
Example and input schema
{
"allowedCountries": [
"AE",
"SA",
"US"
],
"window": {
"start": "09:00",
"end": "20:00",
"days": [
0,
1,
2,
3,
4
]
},
"timezone": "Asia/Dubai"
}
{
"type": "object",
"properties": {
"allowedCountries": {
"description": "ISO 3166-1 alpha-2 codes outbound calls may dial. Empty list = the default (the organization's own country plus the countries of its numbers).",
"maxItems": 50,
"type": "array",
"items": {
"type": "string",
"minLength": 2,
"maxLength": 2
}
},
"window": {
"description": "Default calling hours for campaigns, in each destination's local time.",
"type": "object",
"properties": {
"start": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
"description": "HH:MM local time in the destination's time zone."
},
"end": {
"type": "string",
"pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
"description": "HH:MM."
},
"days": {
"minItems": 1,
"maxItems": 7,
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 6
},
"description": "0 = Sunday … 6 = Saturday."
}
},
"required": [
"start",
"end",
"days"
]
},
"timezone": {
"description": "IANA time zone of the organization.",
"type": "string",
"maxLength": 64
},
"consentText": {
"description": "Outbound consent / recording text agents read at the start of outbound calls (empty = the agent's own).",
"type": "string",
"maxLength": 600
}
}
}
Resources and prompts
Resources are read-only documents a client can attach to its context; prompts are reusable instructions a client can insert as a user message.
| Resource | What it is |
|---|---|
agentcalls://skill | AgentCalls skill (SKILL.md): How to run a phone line through this server end to end: setup → agent → number → test call → inbound/outbound → summaries and webhooks → billing. Read it before a long task; it follows the Agent Skills format (agentskills.io). |
agentcalls://skill/{file} | Skill reference file: A reference file of the skill: workflows.md, tools.md (generated from this server's tool list), safety.md, errors.md. |
agentcalls://templates | Agent templates: The four agent templates (receptionist, appointment booking, lead qualification, support) in English and Arabic: instructions, opening line, consent text, analysis schema and built-in tools. Read one with agentcalls://templates/{template}/{language}. |
agentcalls://templates/{template}/{language} | One agent template: Full text of a template, e.g. agentcalls://templates/receptionist/ar. |
agentcalls://voices | Voice catalog: Every voice with language, gender, accent, tier and preview URL (same data as list_voices). |
agentcalls://voices/{language} | Voices for a language: Voices for `en` or `ar`. |
agentcalls://countries | Number catalogue: Countries, number types, instant-vs-request mode and documents (same data as list_number_countries, without prices). |
use_agentcalls_skill · Load the AgentCalls skill
Inserts the skill (SKILL.md): the end-to-end workflow, the rules about money and confirmations, and where to look things up.
Arguments
{
"type": "object",
"properties": {
"task": {
"description": "What the user wants done, in one sentence (optional).",
"type": "string",
"maxLength": 1000
}
}
}
set_up_phone_line · Set up a phone line from scratch
Guided setup: check the account, design and publish an agent, get a number (buy or request), test it, and hand over.
Arguments
{
"type": "object",
"properties": {
"business": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Business name and what it does, e.g. \"Northwind Dental Clinic in Dubai\"."
},
"country": {
"default": "US",
"description": "Country of the phone number (ISO 3166-1 alpha-2).",
"type": "string",
"minLength": 2,
"maxLength": 2
},
"language": {
"default": "en",
"description": "Language of the agent.",
"type": "string",
"enum": [
"en",
"ar"
]
}
},
"required": [
"business"
]
}
design_receptionist_agent · Design a receptionist agent
Walks through choosing a voice, writing instructions, creating, publishing and assigning a number for a phone receptionist.
Arguments
{
"type": "object",
"properties": {
"business": {
"type": "string",
"minLength": 1,
"maxLength": 200,
"description": "Business name and what it does, e.g. \"Northwind Dental Clinic in Dubai\"."
},
"language": {
"default": "en",
"description": "Language of the agent.",
"type": "string",
"enum": [
"en",
"ar"
]
},
"details": {
"description": "Opening hours, services, prices, booking rules, escalation contact — anything the receptionist must know.",
"type": "string",
"maxLength": 2000
}
},
"required": [
"business"
]
}
review_recent_calls · Review recent calls
Pulls recent calls and summarizes outcomes, sentiment, failures and follow-ups.
Arguments
{
"type": "object",
"properties": {
"since": {
"description": "ISO date-time to start from (default: last 7 days).",
"type": "string",
"maxLength": 40
},
"agentId": {
"description": "Restrict to one agent.",
"type": "string",
"maxLength": 64
}
}
}
place_follow_up_call · Place a follow-up call
Checks the contact and do-not-call status, confirms with the user, then starts the call and reports the result.
Arguments
{
"type": "object",
"properties": {
"contact": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"description": "Contact id or E.164 phone number."
},
"goal": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "What the call should achieve, e.g. \"confirm Thursday's 10:00 appointment\"."
}
},
"required": [
"contact",
"goal"
]
}
run_campaign · Run an outbound campaign
Collects the list, checks compliance and balance, creates the campaign as a draft, reviews rejected rows, then starts it with the user's go-ahead and reports progress.
Arguments
{
"type": "object",
"properties": {
"goal": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "What every call should achieve, e.g. \"remind customers their subscription renews Monday\"."
}
},
"required": [
"goal"
]
}
Agent skill
The AgentCalls skill is a SKILL.md in the Agent Skills format plus reference files (workflows, every tool's arguments, safety rules, error codes). It teaches an agent the whole workflow: get started → agent → number → test call → inbound and outbound → summaries and webhooks → billing, and when to stop and ask you.
- Through MCP: the resource
agentcalls://skill(andagentcalls://skill/{file}) and the promptuse_agentcalls_skill. Claude and ChatGPT read them on demand. - Claude Code and other agents: download the zip and unpack it into your skills folder (for example
~/.claude/skills/agentcalls), or upload it as a skill in Claude. - Humans: read it on the site; it is also linked from
/llms.txt.
Security and permissions
Sign in with OAuth 2.1: the client discovers the authorization server from the endpoint (RFC 9728 protected resource metadata), identifies itself with its published Client ID Metadata Document or registers dynamically (RFC 7591), and sends the user to an AgentCalls consent screen where they pick the organization and the permissions. Access tokens last an hour and refresh silently; refresh tokens rotate.
API keys still work for scripts and CI: create one under Settings → Developers and send it as Authorization: Bearer ak_live_….
Permissions are scopes shared by keys and connections; a tool outside the connection's scopes answers 403 insufficient_scope so the client can ask you to re-approve with the missing permission (step-up). Revoke a connection any time under Settings → Developers → Connected AI apps; the tokens stop working immediately.
The endpoint is a stateless Streamable HTTP server: no sessions, no server-initiated messages, every request independent.
Permission groups on the consent screen and the scopes they grant (API keys use the same scopes):
| Group | Scopes | Default |
|---|---|---|
| agents | agents:read agents:write voices:read | on |
| calls | calls:read calls:write | on |
| contacts | contacts:read contacts:write | on |
| numbers | numbers:read numbers:write | on |
| campaigns | campaigns:read campaigns:write | on |
| billing | usage:read billing:read | on |
| integrations | integrations:read | on |
| webhooks | webhooks:read webhooks:write | off |
| organization | org:read org:write | on |
| Scope | Tools |
|---|---|
org:read | get_started, get_organization |
agents:read | list_agents, get_agent, test_agent |
agents:write | create_agent, update_agent, publish_agent |
voices:read | list_voices |
numbers:read | list_number_countries, list_numbers, get_number, search_numbers |
numbers:write | buy_number, request_number, assign_number, release_number |
calls:write | start_call, end_call |
calls:read | get_call, get_transcript, get_recording_url, list_calls, list_live_calls |
campaigns:read | list_campaigns, get_campaign, get_campaign_results |
campaigns:write | create_campaign, set_campaign_status |
contacts:read | list_contacts, get_contact |
contacts:write | upsert_contact, set_do_not_call |
usage:read | get_usage |
billing:read | list_invoices, get_topup_link, get_plan_link |
integrations:read | list_integrations, get_integration_connect_link |
webhooks:read | list_webhooks |
webhooks:write | create_webhook, test_webhook |
org:write | update_outbound_settings |
Default connection scopes: agents:read agents:write voices:read calls:read calls:write contacts:read contacts:write numbers:read numbers:write campaigns:read campaigns:write usage:read billing:read integrations:read org:read org:write. Everything an AI app does is written to the organization's audit log with the name of the person who approved the connection.
{
"discovery": {
"protected_resource": "https://agentcalls.net/.well-known/oauth-protected-resource/mcp",
"authorization_server": "https://agentcalls.net/.well-known/oauth-authorization-server"
},
"endpoints": {
"authorize": "https://agentcalls.net/oauth/authorize",
"token": "https://agentcalls.net/oauth/token",
"register": "https://agentcalls.net/oauth/register",
"revoke": "https://agentcalls.net/oauth/revoke",
"introspect": "https://agentcalls.net/oauth/introspect"
},
"grants": [
"authorization_code (PKCE S256)",
"refresh_token (rotating)"
],
"client_registration": [
"Client ID Metadata Documents",
"Dynamic Client Registration (public clients)"
],
"redirect_uris": "https, loopback (any port), private app schemes"
}
Money and safety
buy_number,request_number,start_call,create_campaign(when started) andset_campaign_status runningspend money;end_callhangs up on a person;release_numberand cancelling a campaign are irreversible. All of them requireconfirm: true, and the tool descriptions tell the model to ask the user first.start_calldials a real phone. Contacts marked do-not-call are refused; the organization's allowed countries, calling hours, concurrency limit and billing state are enforced exactly as in the dashboard.- Calls are asynchronous: start_call returns while the phone rings. Poll
get_call(the call card does this for you) or subscribe to thecall.summary.readywebhook; a tool call never waits for a phone call. - Every mutating tool writes an audit row (
mcp.<tool>) with the signed-in user (OAuth) or the key id, visible to administrators.
Limits and errors
- 60 requests per minute per connection or key (shared with the REST API), enforced per server instance; a 429 carries
Retry-After. - Lists return at most 100 items per page;
nextCursorpaginates. Results stay well under hosts' size limits (about 150,000 characters on Claude). - Tool results are JSON in a text block and, for clients that read it, the same object as
structuredContent. Hosts that support MCP Apps also get an interactive card for the tools marked below.
Tool failures are returned as isError: true results with a JSON body { error: { code, message, details? } }. Codes match the REST API (invalid_request, forbidden, not_found, agent_not_published, number_not_active, payment_required, concurrency_limit, contact_do_not_call, country_not_allowed, call_start_failed, …) plus confirmation_required, validation_failed (agent configuration issues) and not_acknowledged (campaigns before the compliance acknowledgement).
{
"error": {
"code": "confirmation_required",
"message": "A call costs money and rings a real phone. Ask the user, then call start_call again with confirm: true."
}
}
HTTP-level failures use the REST API envelope and status codes: 401 unauthorized (no, expired or revoked token / key; carries the discovery challenge), 403 forbidden (origin not allowed, or insufficient_scope for step-up), 429 rate_limited with Retry-After.
FAQ
Do I need an API key?
No. Paste the URL into Claude or ChatGPT and sign in; the connection gets its own tokens that you can revoke under Settings → Developers → Connected AI apps. API keys remain for scripts, CI and clients that cannot sign in.
Can the AI spend my money without asking?
No. Buying or requesting a number, starting a call or campaign, hanging up and releasing a number all require confirm: true, and the tool descriptions and the skill instruct the model to ask you first. Links for top-ups and plan changes open hosted checkout pages where you pay yourself.
Which organization does the AI see?
The one you picked on the consent screen. Connect again to add another organization; each connection is one organization with its own permissions.
Does it work in Arabic?
Yes. Tool outputs are plain language, the cards follow the organization's language (English or Arabic, right-to-left), and agents, voices and templates exist in both languages.
What about ChatGPT?
ChatGPT connects with the same URL in developer mode (OAuth). It renders the same interactive cards through the MCP Apps extension; AgentCalls also sets the compatibility keys ChatGPT reads.
Can I limit what the AI may do?
Yes, twice: untick permission groups on the consent screen (the connection then cannot call those tools and the client asks you again if it needs them), and in Claude block individual tools under Customize → Connectors.
Where are the tokens stored?
Only a keyed hash of each token is stored, like API keys. Access tokens last one hour and refresh silently; refresh tokens rotate on every use and a reused one revokes the whole connection.
Protocol details
Transport: Streamable HTTP, one POST per JSON-RPC message, JSON responses (SSE only when a tool streams notifications). Protocol versions served: 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26. Clients on the 2025 revisions may send initialize; it is answered statelessly, no session id is minted, and GET or DELETE on the endpoint answer 405. The MCP Apps extension (io.modelcontextprotocol/ui, views as text/html;profile=mcp-app) is advertised through tool metadata; hosts without it ignore it.
Browsers may not call the endpoint cross-site (an Origin header other than agentcalls.net is refused with 403); every supported client connects from a server or a desktop process and sends none. The server is built on the official TypeScript SDK (@modelcontextprotocol/server).
Prefer REST? The same keys work on the REST API v1; its OpenAPI document lists this endpoint under x-mcp.