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.

ClientConnect withCards
Claude (web, desktop, mobile)URL + sign inYes
ChatGPT (developer mode)URL + sign inYes
Claude CodeURL + sign in, or API keyText results
CursorURL + sign in, or API keyText results
Any MCP client or SDKURL + sign in, or API keyText results

Connect in 60 seconds

Claude's Add custom connector dialog: a name field and an MCP server URL field

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

https://agentcalls.net/mcp

Authentication

OAuth

ChatGPT → Settings → Apps & Connectors → Developer mode → Create (illustration of the form).

Claude (web, desktop, mobile)

  1. Open Customize → Connectors → Add custom connector (Team/Enterprise: Organization settings → Connectors → Add → Custom).
  2. Name: AgentCalls. MCP server URL: https://agentcalls.net/mcp. Leave the OAuth client settings on their defaults ("Use Claude's published identity").
  3. Click Add, then Connect: an AgentCalls sign-in opens; pick the organization and the permissions, and approve.
  4. Ask Claude: "Where am I with AgentCalls?" — it calls get_started and shows the setup card.

ChatGPT (developer mode)

  1. Settings → Apps & Connectors → Advanced → turn on Developer mode (Plus, Pro, Business, Enterprise, Edu).
  2. Connectors → Create: Name AgentCalls, MCP server URL https://agentcalls.net/mcp, Authentication: OAuth. Save.
  3. Open a new chat, enable the AgentCalls connector from the tools menu and sign in when asked: pick the organization and approve.

Claude Code

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

  1. 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 headers block 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

  1. 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:

  1. The client calls the endpoint without a token and receives 401 with WWW-Authenticate: Bearer resource_metadata=… (RFC 9728 protected resource metadata at https://agentcalls.net/.well-known/oauth-protected-resource/mcp), which names the authorization server and the default permissions.
  2. 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).
  3. 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/token for an access token (1 hour) and a rotating refresh token (30 days).
  4. 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

ToolScopeKindSummary
get_startedorg:readreadcardWhere 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

ToolScopeKindSummary
list_agentsagents:readreadList agents
get_agentagents:readreadcardGet an agent
create_agentagents:writewritecardCreate an agent
update_agentagents:writewritecardUpdate an agent
publish_agentagents:writewritecardPublish an agent
test_agentagents:readreadTest an agent
list_voicesvoices:readreadList 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

ToolScopeKindSummary
list_number_countriesnumbers:readreadCountries where numbers are offered
list_numbersnumbers:readreadcardList phone numbers
get_numbernumbers:readreadcardGet a phone number
search_numbersnumbers:readreadcardSearch available numbers
buy_numbernumbers:writewritemoneycardBuy a phone number
request_numbernumbers:writewritemoneyRequest a number that needs registration
assign_numbernumbers:writewritecardAssign a number to an agent
release_numbernumbers:writewriteirreversibleRelease 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)

ToolScopeKindSummary
start_callcalls:writewritemoneycardStart an outbound call
end_callcalls:writewritecardEnd a live call
get_callcalls:readreadcardGet a call
get_transcriptcalls:readreadGet a transcript
get_recording_urlcalls:readreadGet a recording link
list_callscalls:readreadcardList calls
list_live_callscalls:readreadcardList 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

ToolScopeKindSummary
list_campaignscampaigns:readreadList campaigns
get_campaigncampaigns:readreadcardGet a campaign
get_campaign_resultscampaigns:readreadGet campaign results
create_campaigncampaigns:writewritemoneycardCreate a campaign
set_campaign_statuscampaigns:writewritemoneycardStart, 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

ToolScopeKindSummary
list_contactscontacts:readreadcardList contacts
get_contactcontacts:readreadcardGet a contact
upsert_contactcontacts:writewritecardCreate or update a contact
set_do_not_callcontacts:writewritecardSet 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

ToolScopeKindSummary
get_usageusage:readreadcardBalance, plan and usage
list_invoicesbilling:readreadList invoices
get_topup_linkbilling:readreadcardTop-up link
get_plan_linkbilling:readreadcardPlan 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

ToolScopeKindSummary
list_integrationsintegrations:readreadcardList integrations
get_integration_connect_linkintegrations:readreadcardConnect-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

ToolScopeKindSummary
list_webhookswebhooks:readreadcardList webhooks
create_webhookwebhooks:writewritecardCreate a webhook
test_webhookwebhooks:writewritecardSend 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

ToolScopeKindSummary
get_organizationorg:readreadcardGet organization settings
update_outbound_settingsorg:writewritecardUpdate 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.

ResourceWhat it is
agentcalls://skillAgentCalls 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://templatesAgent 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://voicesVoice 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://countriesNumber 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 (and agentcalls://skill/{file}) and the prompt use_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):

GroupScopesDefault
agentsagents:read agents:write voices:readon
callscalls:read calls:writeon
contactscontacts:read contacts:writeon
numbersnumbers:read numbers:writeon
campaignscampaigns:read campaigns:writeon
billingusage:read billing:readon
integrationsintegrations:readon
webhookswebhooks:read webhooks:writeoff
organizationorg:read org:writeon
ScopeTools
org:readget_started, get_organization
agents:readlist_agents, get_agent, test_agent
agents:writecreate_agent, update_agent, publish_agent
voices:readlist_voices
numbers:readlist_number_countries, list_numbers, get_number, search_numbers
numbers:writebuy_number, request_number, assign_number, release_number
calls:writestart_call, end_call
calls:readget_call, get_transcript, get_recording_url, list_calls, list_live_calls
campaigns:readlist_campaigns, get_campaign, get_campaign_results
campaigns:writecreate_campaign, set_campaign_status
contacts:readlist_contacts, get_contact
contacts:writeupsert_contact, set_do_not_call
usage:readget_usage
billing:readlist_invoices, get_topup_link, get_plan_link
integrations:readlist_integrations, get_integration_connect_link
webhooks:readlist_webhooks
webhooks:writecreate_webhook, test_webhook
org:writeupdate_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) and set_campaign_status running spend money; end_call hangs up on a person; release_number and cancelling a campaign are irreversible. All of them require confirm: true, and the tool descriptions tell the model to ask the user first.
  • start_call dials 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 the call.summary.ready webhook; 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; nextCursor paginates. 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.