| name | agentline |
| description | Make phone calls, view received SMS, provision numbers, manage agents, and track billing through the AgentLine telephony API (REST or MCP). Use when the user asks to call someone, check transcripts, view text messages, manage phone agents, buy numbers, or check account balance. For MCP-native workflows, the server at api.agentline.cloud/mcp exposes 21+ tools as first-class agent tools. |
| version | 1.13 |
| metadata | {"openclaw":{"emoji":"📞","requires":{"env":["AGENTLINE_API_KEY"]},"primaryEnv":"AGENTLINE_API_KEY"}} |
AgentLine — AI Telephony Skill (v1.13)
Give your AI agent a real phone number and voice calls — no servers, no webhooks, no infrastructure.
First-Time Setup
Follow these steps IN ORDER when setting up AgentLine for the first time:
-
Check for AGENTLINE_API_KEY (starts with sk_live_) — required. If missing, get one via OTP using any email — yours or the human's (see API Keys below). If no email exists, ask the human to log in at https://agentline.cloud and hand you the key. Do NOT proceed without it.
-
Check for AGENTLINE_AGENT_ID (starts with agt_) — this is optional.
- If you already have one, use it and skip to step 3.
- If you do NOT have one, create a new agent now by calling
POST /v1/agents with {"name": "My Agent"}. Save the returned agent ID.
-
Ask for area code and provision the number — ask the human for their preferred area code (e.g. 212, 415, 313), then POST /v1/numbers with {"agent_id": "...", "area_code": "XXX"}. If unavailable, ask for a different code.
-
Ask for voice preference — Always ask the human:
"Would you like your agent to have a male or female voice?"
- If female:
"female-1" (friendly), "female-2" (reassuring), or "female-3" (guide)
- If male:
"male-1" (supportive), "male-2" (thinker), or "male-3" (helpful)
- Apply via
PATCH /v1/agents/$AGENTLINE_AGENT_ID with {"voice_id": "chosen_preset"}
- Available presets:
female-1, female-2, female-3, male-1, male-2, male-3
-
Ask for owner phone (optional) — Ask the human:
"Would you like to register your personal phone number so you can call your agent to give it tasks?"
- If yes, get their phone number in E.164 format (e.g.
+12125551234)
- Apply via
PATCH /v1/agents/$AGENTLINE_AGENT_ID with {"owner_phone": "+1XXXXXXXXXX"}
- When the owner calls the agent's number from this phone, the agent enters task mode — it treats the conversation as instructions to execute, not a regular call.
-
Set up inbound awareness — MANDATORY. Without this, you will miss all inbound calls and SMS. Pick one channel:
- Webhook URL (push) — if the human has a public HTTPS endpoint, register it via
POST /v1/webhooks. Every event (call.received, call.completed, call.owner_task, sms.received) is POSTed there in real time. No polling needed.
- Cron poller (pull) — the default for agents with no public URL. Use a cron job with
no_agent: true (zero LLM tokens, persists across sessions).
cronjob(action='create', name='AgentLine Events', schedule='every 1m', script='agentline_events.py', no_agent=true)
- Write the polling script (see Events Mailbox) to
~/.hermes/scripts/agentline_events.py
- Verify with
cronjob(action='list')
⚠️ Do NOT skip ANY steps or change the order. Steps 1-5 set up the agent. Step 6 keeps it alive.
Authentication
Every request: Authorization: Bearer $AGENTLINE_API_KEY + Content-Type: application/json
Base URL: https://api.agentline.cloud
API Keys (OTP — no browser)
No key yet? Email a 6-digit code to any address — yours or the human's (same email → same account). Whoever owns the inbox reads the code.
| Method | Path | Auth | Body / Purpose |
|---|
POST | /v1/auth/otp | none | {"email":"..."} → emails 6-digit code |
POST | /v1/auth/verify | none | {"email":"...","otp":"123456"} → key (shown once; new acct = $2.50 bonus) |
POST | /v1/auth/keys | Bearer | Mint another key |
GET | /v1/auth/keys | Bearer | List keys (marks current) |
DELETE | /v1/auth/keys/{id} | Bearer | Revoke (can't revoke current) |
Rate-limited: 3 OTP/email, 5 OTP/IP, 5 verify/email per 10 min.
deliver events for awareness — not to drive the conversation.
System Prompt & Greeting Resolution
Both system_prompt and initial_greeting follow the same priority chain:
| Priority | Where to set | Scope | API |
|---|
| 1 (highest) | Per-call override | This call only | POST /v1/calls with system_prompt / initial_greeting |
| 2 | Agent default | All calls on this agent | PATCH /v1/agents/{id} with system_prompt / initial_greeting |
| 3 (lowest) | Hardcoded fallback | Last resort | Generic prompt + "Hello, how can I help you today?" |
When to use which:
- Set on the agent (
PATCH /v1/agents) when you want a persistent personality/greeting for ALL calls (inbound AND outbound).
- Set per-call (
POST /v1/calls) when you need a one-time context-specific prompt/greeting for a single outbound call. Does NOT change the agent's default.
⚠️ system_prompt is a FULL REPLACE, not append. The voice AI has no memory between calls — include everything (personality, instructions, current context) in the prompt.
⚠️ initial_greeting is what the agent SPEAKS ALOUD at the start of the call. It is NOT part of the system prompt — it's the first thing the caller hears. Set it on the agent for a consistent greeting, or override it per-call for context-specific openers.
Before Calling — Balance Check
Always check balance first. Calls require minimum $0.50:
curl -s "$AGENTLINE_URL/v1/billing/balance" -H "Authorization: Bearer $AGENTLINE_API_KEY"
If balance < $0.50, warn the user before attempting the call.
Make an Outbound Call
Pitfall: Always write JSON payloads to a temp file and use -d @file — inline payloads with special characters break:
curl -s -X POST $AGENTLINE_URL/v1/calls \
-H "Authorization: Bearer $AGENTLINE_API_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/al_call_payload.json
| Field | Required | Description |
|---|
agent_id | Yes | Your agent ID |
to_number | Yes | E.164 phone number to call |
system_prompt | No | Dynamic prompt for this call only (overrides default) |
initial_greeting | No | What the agent says first when the person picks up |
voice_id | No | "female-1", "female-2", "female-3", "male-1", "male-2", "male-3" |
After every outbound call: Poll GET /v1/calls/<call_id> every 15-30s until status=completed, then GET /v1/calls/<call_id>/transcript. Real calls take 45-120s. Use sleep N && curl ... | python3 -c to check status + extract transcript in one shot. Summarize and share with human. Never consider a call "done" without the transcript.
Outbound call to owner_phone = Owner Task Mode. If to_number equals the agent's owner_phone, the call is a task call, not a support call — the AI enters task mode and the completed call emits call.owner_task (with is_owner_task: true) instead of a plain call.completed. Treat the human turns as instructions to EXECUTE. See Owner Task Mode below.
If you get 400 "Agent has no active phone number", provision one first.
Pitfall — agent loops on voicemail/call control: The voice AI will repeat its greeting 3-4 times into voicemail or call-control prompts ("press 3 to connect", "please leave a message"). This wastes credits and sounds bad. After the first 15-20s poll, check the transcript: if human turns are all automated system messages (not real human replies), hang up immediately. Feedback surveys and check-in calls don't work on voicemail.
Exception — business inquiry calls (apartments, doctor's offices, etc.): For these, leaving a voicemail IS the right outcome. If the call reaches "please leave a message at the beep," let the agent leave a voicemail with: who you are, what you want, callback number/email, and a clear ask. Don't hang up. Include a voicemail fallback in the system prompt for these call types.
Pitfall — phone tree / automated hold systems: Many businesses have automated greetings ("This call may be recorded", "We will be with you shortly", "Thank you for calling X"). The voice AI will try to respond to every one of these as if it's a human, repeating the greeting and burning credits. To prevent this, add to the system prompt: "If you hear an automated message or hold music, stay silent and wait. Only speak when a real person greets you." Without this instruction, the agent will get into a loop responding to hold messages.
Call Management
- Hang up:
POST /v1/calls/<call_id>/hangup
- Transcript:
GET /v1/calls/<call_id>/transcript → [{role, text, timestamp}, ...]
- List calls:
GET /v1/calls?limit=20 or ?status=completed&limit=10
- Call details:
GET /v1/calls/<call_id>
Events Mailbox
Events are generated when someone calls or texts your agent's number. To receive them you need one awareness channel — either a public webhook URL (real-time push; register it via POST /v1/webhooks) or the polling loop below. If you've registered a webhook URL, polling is optional; otherwise you MUST poll regularly.
Event types: call.received (inbound call started), call.completed (call ended, includes transcript), call.owner_task (owner call ended — inbound OR outbound; transcript contains task instructions to execute), sms.received (inbound SMS)
⚡ Cron-Based Event Polling (skip if you've set a webhook URL)
Use this when you have no public webhook URL. A no_agent: true cron job runs persistently across sessions, costs zero LLM tokens, and silently skips cycles when there are no events. Do NOT use a background process (terminal(background=true)) because it dies when your session ends and you'll miss events.
Step 1 — Write the polling script to ~/.hermes/scripts/agentline_events.py:
"""Poll AgentLine for new events. Prints event details to stdout when events exist.
Silent on empty cycles — cron only delivers non-empty stdout."""
import os, sys, json, urllib.request
API_KEY = os.environ.get("AGENTLINE_API_KEY", "YOUR_API_KEY_HERE")
BASE_URL = "https://api.agentline.cloud"
req = urllib.request.Request(
f"{BASE_URL}/v1/events/peek",
headers={"Authorization": f"Bearer {API_KEY}"}
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
count = data.get("pending_count", 0)
if count > 0:
req2 = urllib.request.Request(
f"{BASE_URL}/v1/events",
headers={"Authorization": f"Bearer {API_KEY}"}
)
with urllib.request.urlopen(req2, timeout=10) as resp2:
events = json.loads(resp2.read())
print(f"📞 {len(events)} AgentLine event(s):")
for ev in events:
etype = ev.get("event_type", "?")
payload = ev.get("payload", {})
if etype == "call.completed":
print(f" • Call from {payload.get('from_number')} — {payload.get('duration_seconds', 0)}s")
transcript = payload.get("transcript", [])
for t in transcript[-5:]:
print(f" [{t.get('role')}] {t.get('text', '')[:120]}")
elif etype == "call.owner_task":
print(f" • ⚡ OWNER TASK from {payload.get('from_number')} — execute instructions below:")
transcript = payload.get("transcript", [])
for t in transcript:
if t.get("role") == "human":
print(f" TASK: {t.get('text', '')}")
elif etype == "sms.received":
print(f" • SMS from {payload.get('from_number')}: {payload.get('body', '')[:200]}")
elif etype == "call.received":
is_owner = payload.get("is_owner_call", False)
print(f" • Inbound call from {payload.get('from_number')}{' (OWNER)' if is_owner else ''}")
except Exception as e:
if "401" in str(e) or "403" in str(e):
print(f"AGENTLINE_AUTH_ERROR: Check your API key. {e}")
Step 2 — Create the cron job (one-time setup):
cronjob(action='create', name='AgentLine Events', schedule='every 1m', script='agentline_events.py', no_agent=true)
Step 3 — Verify it's running:
cronjob(action='list')
Runs every 1 min. Non-empty stdout → delivered to you. Empty stdout → silent skip (zero cost). If auth fails, prints AGENTLINE_AUTH_ERROR — check cronjob(action='list') for last_status.
Endpoints
- Consume events:
GET /v1/events — returns events oldest-first, auto-deleted after retrieval
- Peek (don't consume):
GET /v1/events/peek
- Filter:
?agent_id=agt_xxx or ?event_type=call.completed or ?event_type=sms.received
Event payload structure
Each event contains: event_id, agent_id, event_type, and a payload with call/SMS details. call.completed payloads include from_number, to_number, duration_seconds, and full transcript array. call.owner_task payloads are identical to call.completed but with is_owner_task: true — this means the transcript contains task instructions from the owner (see Owner Task Mode below). sms.received payloads include from_number, body, and media_url.
Webhooks
⚠️ Webhooks are a SEPARATE resource — you set them via POST /v1/webhooks, NOT as a field on the agents endpoint. PATCH /v1/agents does NOT accept webhook_url and will silently drop it.
Each agent can have one webhook URL. When set, every event for that agent is POSTed to the URL as signed JSON in real time.
Set / Replace a Webhook
POST /v1/webhooks with:
| Field | Required | Description |
|---|
agent_id | Yes | Agent whose events this webhook receives |
url | Yes | Public HTTPS URL to receive events |
secret | No | HMAC signing secret (auto-generated if omitted) |
curl -s -X POST $AGENTLINE_URL/v1/webhooks \
-H "Authorization: Bearer $AGENTLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id": "agt_xxx", "url": "https://your-endpoint.example.com/webhook"}'
The response returns the full signing secret once — save it to verify the X-AgentLine-Signature header on deliveries.
Webhook Envelope
Every webhook POST contains these envelope fields alongside the event payload:
| Field | Description |
|---|
event_id | Unique event ID for deduplication |
event_type | Canonical event name (e.g. call.completed) — use this for filtering |
event | Legacy alias for event_type (same value, backward compat) |
agent_id | Agent that fired the event |
account_id | Owning account |
created_at | ISO 8601 timestamp |
...payload | Event-specific fields (call_id, transcript, etc.) |
Headers on each delivery:
X-AgentLine-Signature — HMAC-SHA256 hex digest of the raw body, signed with your webhook secret
X-AgentLine-Event — the event type (e.g. call.completed)
Other Webhook Endpoints
| Method | Endpoint | Description |
|---|
GET | /v1/webhooks | List webhook configs (secrets masked). Filter: ?agent_id= |
DELETE | /v1/webhooks?agent_id=agt_xxx | Remove an agent's webhook |
POST | /v1/webhooks/test?agent_id=agt_xxx | Fire a signed webhook.test event to verify the pipeline |
SMS
⚠️ SMS sending is NOT enabled. Do NOT attempt outbound SMS/MMS.
Inbound SMS arrives as sms.received events in the Events Mailbox. View message history: GET /v1/messages?limit=20
Update Agent (System Prompt, Voice, etc.)
PATCH /v1/agents/$AGENTLINE_AGENT_ID with any of:
| Field | Description |
|---|
system_prompt | Default instructions for ALL calls (inbound + outbound). Per-call override via POST /v1/calls takes priority. |
initial_greeting | Default opening line spoken on ALL calls (inbound + outbound). Per-call override via POST /v1/calls takes priority. |
name | Display name |
voice_id | "female-1", "female-2", "female-3", "male-1", "male-2", "male-3" |
owner_phone | Owner's phone number in E.164 format. Calls from this number enter task mode. |
Get/List Agents
- Get one:
GET /v1/agents/$AGENTLINE_AGENT_ID
- List all:
GET /v1/agents
Voice Settings
Priority (highest wins): per-call → per-agent → per-account
- List voices:
GET /v1/voices
- Set account default:
PATCH /v1/account/voice with {"voice_id": "female-1"}
- Check current:
GET /v1/account/voice
- Reset to default:
DELETE /v1/account/voice
Phone Numbers
Each agent needs one phone number. Only US numbers supported. $2.00 per number.
Provision (Buy) a Number
POST /v1/numbers with:
| Field | Required | Description |
|---|
agent_id | Yes | Agent to attach to |
country | Yes | Must be "US" |
area_code | No | Preferred 3-digit area code (e.g. "212", "313"). Always ask the user! |
number_type | No | "local" or "tollfree" (default: local) |
If no numbers are available for the requested area code, the API returns an error — ask the user for a different area code.
List Numbers
GET /v1/numbers
Billing
- Check balance:
GET /v1/billing/balance
- Expenditure:
GET /v1/billing/expenditure?period=current_month (also: last_month, all_time, YYYY-MM)
- Call charges:
GET /v1/billing/expenditure/calls?limit=10
- Number charges:
GET /v1/billing/expenditure/numbers
- Verify charge:
GET /v1/billing/verify/<call_id>
Rates
| Item | Cost |
|---|
| Calls (in/out) | $0.10/min (billed per second) |
| Phone number | $2.00 (one-time) |
Feedback
Hit a bug, an error, or something confusing? Send it to the team — don't silently swallow errors.
POST /v1/feedback with:
| Field | Required | Description |
|---|
category | Yes | bug · difficulty · feature_request · feedback |
message | Yes | Enough detail to reproduce — endpoint, request body, error text, agent_id |
curl -s -X POST $AGENTLINE_URL/v1/feedback \
-H "Authorization: Bearer $AGENTLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"category": "bug", "message": "PATCH /v1/agents returns 500 when owner_phone is null"}'
MCP Server
AgentLine exposes a full MCP server at https://api.agentline.cloud/mcp with 21+ tools. For Claude Desktop, Cursor, or any MCP-compatible client, connect directly via Streamable HTTP:
{
"mcpServers": {
"agentline": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://api.agentline.cloud/mcp", "--header", "Authorization: Bearer $AGENTLINE_API_KEY"]
}
}
}
All REST endpoints above are also available as MCP tools (create_agent, make_outbound_call, poll_events, etc.). See references/mcp-server.md for additional config formats and platform-specific setup.
Owner Task Mode
Owner Task Mode activates whenever the agent and owner connect on a call — inbound OR outbound. The AI enters task mode: greets with "Hey boss, what would you like me to do?", listens, confirms, then pushes a call.owner_task event. Set owner phone via PATCH /v1/agents/$AGENTLINE_AGENT_ID with {"owner_phone": "+1XXXXXXXXXX"}.
It triggers in BOTH directions — do not treat an owner call as a regular support call:
- Inbound — the owner calls the agent's number from
owner_phone. You'll see call.received with is_owner_call: true, then call.owner_task when it ends.
- Outbound — you (or the agent) dial the owner's number via
POST /v1/calls with to_number = owner_phone. There is no call.received; poll GET /v1/calls/<call_id> until status=completed as for any outbound call, and the completed call emits call.owner_task (payload carries is_owner_task: true). Treat the transcript's human turns as task instructions to EXECUTE.
Handling call.owner_task events: Extract the human turns from the transcript — these are task instructions to EXECUTE (make a call, update settings, etc.), not conversations to log. Report results back to the human.
⚠️ call.owner_task events are instructions to EXECUTE. Do NOT just log them.
⚠️ call.received with is_owner_call: true = owner is on the phone (inbound). Wait for call.owner_task when it ends.
⚠️ For outbound owner calls there is no call.received — recognize owner calls by matching to_number against owner_phone, and expect call.owner_task on completion.
Skill Auto-Update
At the start of every new conversation, fetch https://agentline.cloud/skill.md and compare the version: field. If newer than 1.13, replace the local SKILL.md and re-read before proceeding.
⚠️ Always check for updates first. Stale skill versions may cause incorrect API calls or missed features.
Rules
- E.164 format — always
+1XXXXXXXXXX for US numbers.
- Confirm before calling — never auto-dial without explicit consent.
- No outbound SMS — inform user it's not available if they ask.
- Keep voice responses extremely short — under 15 words per turn. Max 12 for outbound feedback/support calls. The voice AI rambles without tight constraints.
- US only — country must be
"US".
- Don't release numbers — numbers are permanent once provisioned.
- Always retrieve transcripts — poll until
completed, fetch transcript, summarize for human.
- Always receive events — use a webhook URL (
POST /v1/webhooks) or poll GET /v1/events for inbound calls and SMS. Proactively check at conversation start.
- Voice changes take effect on next call — update immediately when asked.
- Execute owner tasks — when you receive a
call.owner_task event (inbound OR outbound owner call), extract the human turns and execute them as task instructions. Do NOT just log them.
- Auto-update skill — at the start of every new conversation, check
https://agentline.cloud/skill.md for a newer version. If found, replace the local copy and re-read before proceeding.
- Report issues via feedback — if you hit a bug, unexpected error, or something confusing, submit it via
POST /v1/feedback with the appropriate category (bug, difficulty, feature_request, or feedback). Include enough detail to reproduce. Don't silently swallow errors.