Give your agent real phone numbers for SMS, OTP verification, and voice calls via the AgentCall API.
# AgentCall: Phone Numbers for AI Agents
You have access to the AgentCall API for phone numbers, SMS, voice calls, and AI voice calls. Inbound and outbound calls can both be answered or initiated by an AI voice agent.
## Authentication
All authenticated requests require: `Authorization: Bearer <AGENTCALL_API_KEY>`
The API key is available in the `AGENTCALL_API_KEY` environment variable.
## Base URL
`https://api.agentcall.co`
For a complete plain-text API reference: `GET https://api.agentcall.co/llms.txt` (no auth required).
## Cost & Safety: Confirm Before Acting
Several tools below take real-world actions on the user's behalf. Confirm with the user before invoking them, and stay within the scope of what they explicitly asked you to do.
**Billable actions (cost real money. Confirm scope first):**
- `POST /v1/numbers/provision`: recurring monthly cost ($2.00–$2.50/month depending on type). Confirm the user wants a new number; ask which type, country, and label before provisioning.
- `POST /v1/sms/send`: $0.015/message on Pro (10/month free on Free plan). Confirm the recipient number and the message body before sending.
- `POST /v1/calls/initiate`: $0.035/min on Pro. Confirm the destination number and purpose before dialing. Optional `record: true` adds $0.01/min on top.
- `POST /v1/calls/ai`: $0.40/min on Pro. Confirm the destination, the system prompt, and `maxDurationSecs` before dialing. Outbound AI calls reach a real human and accumulate cost per minute.
- Inbound AI voice (`POST /v1/numbers/:id/inbound-config` with `mode: "ai"`). Free tier includes 5 minutes/month with no card required. Once the trial is exhausted, additional inbound AI calls hang up at the carrier with status `trial_exhausted` until the trial resets on the 1st of next month (UTC), OR until the user upgrades to Pro for unlimited inbound AI. Pro inbound AI is per-number configurable in two billing modes: Managed at $0.40/min (default; AgentCall holds the AI provider key) or BYOK at $0.10/min (customer provides their own AI provider key via `set_byok_openai_key`). There is no pay-as-you-go overage path on Free. Confirm the user wants inbound AI enabled on this specific number, and that the system prompt accurately describes their business.
- `POST /v1/numbers/:id/byok-key` (`set_byok_openai_key`): switches a number's inbound AI voice from Managed billing ($0.40/min) to BYOK billing ($0.10/min) and stores the customer-supplied AI provider key for that number. Confirm the user wants to switch billing modes on the specific number, that they have a working key on hand, and that they understand the per-minute rate change before invoking.
- `DELETE /v1/numbers/:id/byok-key` (`disable_byok`): removes the stored BYOK key on a number and reverts it to Managed billing ($0.40/min). Confirm the user wants to revert billing modes; the system prompt, voice, recording flag, and notify block on the number are preserved.
- `POST /v1/numbers/:id/premium-voice` (`set_premium_voice`): enables Premium Voice on a number, or changes which premium voice it uses. Premium Voice is an opt-in Pro add-on that answers inbound calls with a more natural, brandable voice. Curated voice library, all 13 languages, full caller memory, billed at $0.59/min only on the numbers where it is enabled (the 10 standard voices stay the default at $0.40/min). Pass a `voiceId` from `list_premium_voices`. The number must already have inbound AI configured; enabling premium never wipes the rest of the inbound config; idempotent. Confirm the exact numberId, the chosen voice, and that the user accepts the $0.59/min premium rate before invoking. Requires Pro + a card on file + inbound AI configured, else returns 403 / 402 / 409 respectively; surface that error, do not retry.
- `DELETE /v1/numbers/:id/premium-voice` (`disable_premium_voice`): reverts a number to the standard inbound AI voice, stopping the $0.59/min add-on. Preserves the rest of the inbound config. Idempotent. Pro plan only.
- `list_premium_voices` (`GET /v1/calls/premium-voices`, no auth): browse the Premium Voice catalog (id, name, description, gender, accent, sampleUrl). Use the returned id with set_premium_voice.
- Optional `record: true` on AI voice paths. $0.01/min on top of the AI rate. Mention this delta when proposing recording.
- `create_schedule` (`POST /v1/numbers/:id/schedules`): schedules a proactive text the agent sends FIRST (a one-time appointment reminder, or a recurring digest). Each time it fires it sends one outbound SMS ($0.015 on Pro) to a real person who did not just message in, and a recurring schedule keeps firing until cancelled. Confirm the recipient, the message (or the recurrence and timezone), and the timing before creating it. Pro plan only; the number must have inbound AI / `smsMode: "ai"`. Offer `cancel_schedule` as soon as the schedule is no longer needed.
**Irreversible actions:**
- `DELETE /v1/numbers/:id`: releases the number permanently. The same number cannot be re-provisioned. Always confirm before releasing, and warn the user that this is irreversible.
- `POST /v1/calls/:callId/hangup`: terminates an in-flight call. Usually fine but confirm if the call may still be progressing toward its goal.
**External-effect actions (contact real people):**
- Sending SMS, initiating outbound calls, and AI voice calls reach real recipients on real networks. Don't initiate these speculatively. The receiving party should be expecting contact, or the user should explicitly authorize the outreach.
- For two-party-consent states (CA, FL, IL, MD, MA, PA, WA, NV, NH, MT, CT, DE), recording requires disclosure. AgentCall auto-prepends "This call may be recorded for quality." to the spoken `firstMessage` when `record: true` is set on AI voice paths and the message doesn't already mention recording. But you should still mention recording in firstMessage content for clarity if the user is writing one from scratch.
**Built-in API guardrails (don't rely on these as your only check):**
- Free plan hard caps: 1 local number, 10 SMS/month, 5 voice minutes/month, 5 inbound AI voice minutes/month (no card required for the trial), 5 OTP extractions/month. Prevents runaway spend on the free tier.
- Outbound AI voice + call recording require a payment method on file (Pro plan). The API returns 402 with a `setupUrl` if a Pro user has no card. Surface this to the user instead of looping.
- Inbound AI voice past the 5-minute Free trial requires upgrading to Pro. The API returns 403 `plan_limit_inbound_ai_trial_exhausted` with an `upgradeUrl`. There is no pay-as-you-go overage path on Free; the trial resets on the 1st of each month (UTC).
- Rate limits: 100 req/min global; per-route limits on expensive endpoints (10 req/min on AI voice, 20 req/hour on uploads).
- Carrier restrictions: AI voice (in or out) and inbound AI configuration are US/Canada-only; the API returns 400 `carrier_not_supported` for other countries.
### Inbound AI Configuration: Enable Only After Explicit User Confirmation; Monitor Usage; Disable When No Longer Needed
`configure_inbound_ai` and `POST /v1/numbers/:id/inbound-config` with `mode: "ai"` should follow this procedure every time:
**Pre-flight checklist (require the user to confirm each before invocation):**
1. The exact E.164 number to configure. Read it back if the user has more than one number.
2. The system prompt content. Read back at least the WHAT WE DO and PRICING sections so the user can correct anything inaccurate before invocation.
3. The recording setting (`record: true` or `false`). Default to `false`. Confirm explicit opt-in if the user wants recording.
4. The notification email and/or text (`notify.emailTo`, `notify.smsTo`). Confirm the destination belongs to the user and that they want post-call summaries delivered to it.
5. The shortest practical call duration (`maxDurationSecs`). Pick the smallest value that fits the use case rather than relying on the 600-second default.
6. The budget expectation: Free 5 minutes/month with carrier hang-up after exhaustion, or Pro at $0.40/min with no monthly cap. Tell the user which applies and what their expected monthly bill is.
**Post-configuration responsibility:**
- Monitor usage: periodically call `get_usage` or point the user at the dashboard at /billing.
- Disable as soon as the configuration is no longer needed: proactively offer `disable_inbound_ai` (or `DELETE /v1/numbers/:id/inbound-config`) when the user's stated goal has been met (testing finished, campaign over, business closed for the season).
- If the user reports unexpected calls or charges, call `disable_inbound_ai` first, then investigate.
If the user's request is ambiguous (e.g. "set up a phone number" with no type or country, or "send a text" with no recipient), ask one or two clarifying questions before invoking a billable tool.
## Phone Numbers
**Provision a number:**
```
POST /v1/numbers/provision
Body: { "type": "local", "country": "US", "label": "my-agent" }
Types: local ($2/mo), tollfree ($2.50/mo), mobile ($2/mo). All numbers are VoIP-routed via licensed US carriers; not for consumer-platform signup verification (Stripe, WhatsApp, Google, banks).
Response: { "id": "num_xxx", "number": "+12125551234", "type": "local", ... }
```
**List numbers:**
```
GET /v1/numbers
Query: ?limit=20&country=US&type=local
```
**Get number details:**
```
GET /v1/numbers/:id
```
**Release a number (irreversible):**
```
DELETE /v1/numbers/:id
```
**Rename a number or change its inbound AI voice/language (partial update, preserves all other inbound config):**
```
PATCH /v1/numbers/:id
Body: { "label": "Hermes assistant" } // rename only
Body: { "voice": "marin" } // voice only (requires inbound AI already configured)
Body: { "language": "es" } // language only (requires inbound AI already configured)
Body: { "label": "Hermes", "voice": "coral", "language": "fr" } // any combination
Voices: alloy, ash, ballad, cedar, coral, echo, marin, sage, shimmer, verse
Languages: auto, en, es, fr, de, it, pt, nl, ja, ko, zh, hi, ar
Response: full number object including updated `inbound` block.
```
Use this when the user wants to change the voice or language on an existing receptionist. Do NOT call `POST /v1/numbers/:id/inbound-config` for a voice or language change. That endpoint replaces the entire inbound config and would wipe the system prompt, first message, recording flag, and notify block.
## Inbound AI Voice (Free 5 min/month trial, then Pro plan: Managed $0.40/min or BYOK $0.10/min; US and Canada numbers only)
Configure a phone number so incoming calls are answered autonomously by an AI voice agent. The AI follows the system prompt you set.
After every call, AgentCall summarizes the transcript with an LLM and emails a plain-English summary. Caller name (when given), phone, urgency tag, and a 1-2 sentence ask. To an address you configure on the number. Spam calls are auto-suppressed from the email. The structured `call.transcript` webhook event still fires in parallel for programmatic consumers.
**Configure inbound AI on a number:**
```
POST /v1/numbers/:numberId/inbound-config
Body: {
"mode": "ai",
"systemPrompt": "You are the front desk for Acme Plumbing. Greet the caller warmly, take their name and a brief description of the issue, then say someone will call back within 24 hours.",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, thanks for calling Acme Plumbing. How can I help?",
"maxDurationSecs": 300,
"notify": {
"emailTo": "owner@acmeplumbing.com",
"smsTo": "+14155551234",
"businessName": "Acme Plumbing",
"agencyName": "Acme Plumbing"
}
}
```
The `notify` block is optional. Set `notify.emailTo` to receive the post-call summary email; `businessName` is shown in the email subject; `agencyName` is the sign-off line. Set `notify.smsTo` (E.164 phone) to also receive a short text summary of each call (caller, what they wanted, urgency, callback time), US-local senders only, one text per call (spam skipped), reply STOP to opt out, billed as one outbound SMS ($0.015). `emailTo` and `smsTo` are independent: set either, both, or neither. The MCP `configure_inbound_ai` tool also accepts `notify`: agents in Claude Desktop, Cursor, Windsurf, etc. can configure the email and text destinations in the same call as the system prompt, no dashboard handoff needed.
**Spoken language (optional, default 'auto'):** the `language` field controls what language the AI answers in. `'auto'` matches the caller's language naturally and is what existing receptionists do today. Pass a specific ISO-639-1 code to make the AI respond ONLY in that language even if the caller speaks another, useful when the business serves a specific language community. Supported: `auto`, `en` (English), `es` (Spanish), `fr` (French), `de` (German), `it` (Italian), `pt` (Portuguese), `nl` (Dutch), `ja` (Japanese), `ko` (Korean), `zh` (Chinese Mandarin), `hi` (Hindi), `ar` (Arabic). The directive is added at session-build time so the customer's `systemPrompt` stays unchanged when language is swapped. Use `update_number_language` to change just the language without touching anything else.
**Pre-call context webhook (optional):** add a `contextWebhook` block to wire a live context source onto the number. When set, AgentCall POSTs to your HTTPS URL on every inbound call connect (HMAC-signed with `signingSecret`); your endpoint responds with `{"contextBlock":"..."}` and AgentCall merges that string onto the system prompt before the AI answers. Useful for injecting today's brief, current priorities, or recent email signals so the AI speaks with up-to-date data instead of a static prompt. Example:
```
"contextWebhook": {
"url": "https://hermes.your-domain.com/agentcall/precall",
"signingSecret": "<32+ char shared secret>",
"timeoutMs": 1200
}
```
Fail-open: any error in the webhook leaves the call running with the static prompt. Walkthrough at https://agentcall.co/docs/hermes.
**Optional call recording (Pro plan, $0.01/min on top of the AI voice rate):**
Add `record: true` to the body above (or flick the toggle on the dashboard's `/numbers` config form) to record every inbound call to this number. Recordings live in your AgentCall dashboard for 1 year and are listenable from the Logs → Calls tab or via `GET /v1/calls/:callId/recording` (returns a fresh short-lived signed URL). The `call.recording` webhook fires when each recording is ready. The same `record` flag is supported on outbound AI voice (`POST /v1/calls/ai`). When recording is on, AgentCall auto-prepends a TCPA-compliant disclosure to the spoken `firstMessage` if it doesn't already mention recording.
**Get current inbound config:**
```
GET /v1/numbers/:numberId/inbound-config
```
**Disable inbound AI (calls hang up at carrier):**
```
DELETE /v1/numbers/:numberId/inbound-config
```
### Voice billing mode: Managed vs BYOK (per number)
Each Pro number with inbound AI configured chooses one of two billing modes for its AI voice minutes:
- **Managed** (default): $0.40/min. AgentCall holds the AI provider key on the customer's behalf. Simplest setup; no key handling on the customer side.
- **BYOK**: $0.10/min. The customer provides their own AI provider key; AgentCall bills only the platform overhead. The same number's system prompt, voice, recording flag, and notify block are preserved across mode switches.
Memory, transcripts, post-call email and text summaries, the `call.transcript` and `call.report.ready` webhook events, and call recording all work identically on both modes.
**Switch a number to BYOK (set or rotate the key):**
```
POST /v1/numbers/:numberId/byok-key
Body: { "openaiApiKey": "sk-..." }
Response: { "voiceMode": "byok", "hasByokKey": true, "byokOpenaiApiKeyPreview": "sk-...abcd" }
```
**Switch a number back to Managed (revert billing mode):**
```
DELETE /v1/numbers/:numberId/byok-key
Response: { "voiceMode": "managed", "hasByokKey": false, "byokOpenaiApiKeyPreview": null }
```
**Inspect the current mode** (the existing inbound-config response now includes voiceMode + hasByokKey + byokOpenaiApiKeyPreview):
```
GET /v1/numbers/:numberId/inbound-config
Response: {
...inbound config fields...,
"voiceMode": "managed" | "byok",
"hasByokKey": boolean,
"byokOpenaiApiKeyPreview": "sk-...abcd" | null
}
```
The stored key is write-only; the GET response surfaces a redacted preview (last 4 chars) and a boolean, never the full value. Rotating is a re-POST to `byok-key` with the new value; AgentCall replaces the stored value atomically.
## Browse Voices and Prompt Templates (no auth)
Both endpoints are public. Call them before configuring AI voice to avoid hallucinated business details and to pick a voice that fits the use case.
**List the 10 voices with samples:**
```
GET /v1/calls/voices
Returns: { voices: [{ id, name, trait, description, bestFor, sampleUrl? }, ...], defaultVoice: "shimmer" }
```
The newest natural-sounding picks are `marin` (soft, natural) and `cedar` (warm, grounded). The original 8 (`shimmer`, `sage`, `ash`, `ballad`, `coral`, `echo`, `verse`, `alloy`) ship with sample MP3s; marin and cedar are live-preview only for now.
**List the premium voices (`list_premium_voices`):**
```
GET /v1/calls/premium-voices
Returns: { voices: [{ id, name, description, gender, accent, sampleUrl }, ...], pricing: { premiumVoice: "$0.59/minute" } }
```
Premium Voice is the higher-quality, brandable tier for inbound AI receptionists: a curated voice library, all 13 languages, and full caller memory, a Pro add-on at $0.59/min. Each entry's `id` is the voiceId; preview it via the returned `sampleUrl`. Enable it on a number with `set_premium_voice` (numberId + voiceId), or from the dashboard (a number's inbound AI settings, then the Premium Voice section); revert with `disable_premium_voice`. As a billable add-on, confirm the user accepts the $0.59/min premium rate on the specific number before enabling.
**List ready-made prompt templates:**
```
GET /v1/calls/prompt-templates
Returns 5 templates with [BRACKETED] placeholders to fill in:
- receptionist (Front Desk). Recommends shimmer voice
- lead-qualifier (Sales, BANT-style). Coral
- appointment-booker. Sage
- customer-support (FAQ Deflection). Ash
- call-screener (Anti-Spam). Verse
Each entry includes: id, title, description, recommendedVoice, maxDurationSecs, firstMessage, systemPrompt.
```
For a comprehensive prompt-writing guide: https://agentcall.co/docs/voice-prompts
## SMS
**Send SMS:**
```
POST /v1/sms/send
Body: { "from": "num_xxx", "to": "+14155551234", "body": "Hello!" }
"from" can be a number ID or E.164 phone string
```
**Get inbox:**
```
GET /v1/sms/inbox/:numberId
Query: ?limit=20&otpOnly=true
```
**Get a specific message:**
```
GET /v1/sms/:messageId
```
**Wait for OTP code (long-polls up to 60 seconds):**
```
GET /v1/sms/otp/:numberId
Query: ?timeout=60000
Response: { "otp": "482913", "message": { ... } }
```
## Two-Way AI SMS and Relay Mode
A number can answer inbound texts, not just parse OTPs. Set this on the same inbound config endpoint that sets up the AI receptionist (POST /v1/numbers/:numberId/inbound-config) via the `smsMode` field:
- `smsMode: "ai"`: AgentCall's AI answers each inbound text following your prompt, remembers the contact across voice and SMS, and can return real quotes by calling tools you declare. Declare them with `tools` and host them at `actionWebhook` (HMAC-signed, the same security model as the pre-call context webhook). The AI calls your tool, you return the real value, the AI texts it back instead of guessing. Pro plan, managed only, billed as one outbound text per reply.
- `smsMode: "relay"`: AgentCall runs no AI of its own. It forwards each inbound text to your own agent at `agentWebhook` (HMAC-signed), and your agent replies on its own schedule, so you can text your own agent on a real number. Use `allowedSenders` (E.164 array, up to 20) to restrict which numbers can reach the agent, so a personal agent answers only its owner. Billed as plain texts, no relay fee.
Config fields on configure_inbound_ai: `smsMode` ("off" | "ai" | "relay"), `smsSystemPrompt`, `actionWebhook`, `tools`, `agentWebhook`, `allowedSenders`.
**Read SMS threads:**
```
GET /v1/sms-conversations # list threads (paginated)
GET /v1/sms-conversations/:id # one thread + last 50 messages
```
MCP tools: list_sms_conversations, get_sms_conversation.
**Reply into a thread (relay mode):**
```
POST /v1/sms-conversations/:id/reply
Body: { "body": "3 emails need you today.", "idempotencyKey": "turn_8842" }
```
Your own agent calls this when it has an answer; AgentCall sends and threads the text. Opt-out-checked (refuses to text someone who sent STOP) and idempotent on `idempotencyKey` for 24 hours so a retried agent never double-texts. MCP tool: reply_to_sms_conversation. Billed as one outbound text.
STOP and UNSUBSCRIBE are always honored before either mode runs. Both are inbound-only in v1 (the customer texts first).
## Proactive Scheduling (the agent texts first)
Two-way AI SMS is reactive: the number answers when someone texts it. Proactive scheduling makes a number's AI agent reach out FIRST on a schedule, so it can send appointment reminders and recurring digests without the contact messaging in. The number must have inbound AI / `smsMode: "ai"` (Pro plan). A reply to a proactive message flows back into the same AI thread, so a one-word answer like "C" is understood in context.
```
POST /v1/numbers/:numberId/schedules # create a schedule
GET /v1/numbers/:numberId/schedules # list schedules (?status= optional)
DELETE /v1/numbers/:numberId/schedules/:id # cancel + remove
```
Each schedule is one of two timings and one of two message styles:
- Timing: `fireAt` (a one-time ISO 8601 instant in the future, for reminders) OR `recurrence` (`{ frequency: "daily" | "weekly", hour, minute, dayOfWeek }` interpreted in `timezone`, for digests). Provide exactly one.
- Message: `template` (sent verbatim, with `{{placeholders}}` filled from `payload`) OR `promptHint` (the agent composes the message using its memory of the contact). Provide exactly one.
Pass `dedupeKey` to make a re-create safe: a duplicate key on the same number returns 409, so a calendar sync can create the same reminder repeatedly without duplicating it.
```
POST /v1/numbers/num_abc/schedules
Body: {
"kind": "reminder",
"contactPhone": "+14155551234",
"fireAt": "2026-06-09T19:00:00Z",
"template": "Hi {{name}}, reminder: your {{service}} is tomorrow at {{time}}. Reply C to confirm.",
"payload": { "name": "Marcus", "service": "cleaning", "time": "2pm" },
"dedupeKey": "reminder:appt_8841:24h"
}
```
MCP tools: create_schedule, list_schedules, cancel_schedule. A calendar integration is just a client of create_schedule: post one reminder per appointment with a stable dedupeKey. Each send is billed as one outbound text ($0.015 on Pro); confirm recipient, message, and timing with the user before scheduling, and offer cancel_schedule when it is no longer needed.
## Outbound Voice Calls
**Start a standard outbound call:**
```
POST /v1/calls/initiate
Body: { "from": "num_xxx", "to": "+14155551234", "record": false }
```
**Start an outbound AI voice call (Pro plan, $0.40/min. Billable, contacts real human, confirm with user first):**
The AI handles the entire conversation autonomously based on your systemPrompt.
```
POST /v1/calls/ai
Body: {
"from": "num_xxx",
"to": "+14155551234",
"systemPrompt": "You are calling to schedule a dentist appointment for Tuesday afternoon.",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, I'd like to schedule an appointment please.",
"maxDurationSecs": 600,
"record": false
}
```
Add `record: true` to capture an mp3 of the call. Adds $0.01/min on top.
**Spoken language on outbound calls (optional, default 'auto'):** the `language` field controls what language the AI speaks during the call. `'auto'` lets the AI match the recipient's language naturally. Pass a specific ISO-639-1 code to make the AI respond ONLY in that language. Useful for booking appointments or requesting info on behalf of someone in another language, e.g. `language: 'es'` to call a Spanish-speaking doctor's office on behalf of a customer who doesn't speak Spanish. Supported: `auto`, `en`, `es`, `fr`, `de`, `it`, `pt`, `nl`, `ja`, `ko`, `zh`, `hi`, `ar`. The `firstMessage` stays verbatim, so if you want the greeting in Spanish, write the firstMessage in Spanish yourself; the language setting only governs the AI's responses after.
**Outbound prompt templates** (no auth, public): fetch `GET /v1/calls/prompt-templates?direction=outbound` for three ready-made outbound templates with `[BRACKETED]` placeholders to fill in: `outbound-appointment-booker` (book an appointment on behalf of someone), `outbound-info-request` (call to ask a specific question on behalf of someone), `outbound-callback-confirmation` (follow up on something).
Voices (10 total, default: shimmer; preview the original 8 via GET /v1/calls/voices, marin and cedar are live-preview only):
- marin: soft, natural. New top pick for receptionist, support, conversational small business
- cedar: warm, grounded. New. Advisory, healthcare, trust-building calls
- shimmer: bright, energetic. Long-running default
- sage: calm, authoritative, confident. Healthcare, finance, advisory
- ash: warm, conversational. Customer service, support lines
- ballad: expressive, melodic. Engaging, narrative conversations
- coral: clear, professional. B2B calls, sales
- echo: resonant, deep. Formal inquiries, executive comms
- verse: smooth, articulate. Premium or luxury, executive communication
- alloy: neutral, balanced. Generic notifications, IVR-style flows
### Saved outbound AI agent per number (Pro plan)
Save a reusable outbound AI agent on a phone number so repeat outbound calls from the same number reuse the same configuration. The dashboard Place AI call dialog hydrates from this blob; MCP clients can read it back instead of asking the user to retype the prompt every call. This is independent of the inbound AI receptionist on the same number; configuring one does not change the other.
The saved blob holds: `systemPrompt`, `voice`, `language`, `firstMessage`, `maxDurationSecs`, `record`, and an optional `templateId` (the outbound prompt template used to seed it, when applicable).
**Save the outbound agent on a number:**
```
POST /v1/numbers/:numberId/outbound-defaults
Body: {
"systemPrompt": "You are calling on behalf of Acme Plumbing to confirm tomorrow's 10am appointment.",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, calling to confirm tomorrow's appointment.",
"maxDurationSecs": 300,
"record": false,
"templateId": "outbound-callback-confirmation"
}
```
**Read the saved outbound agent (returns null if none set):**
```
GET /v1/numbers/:numberId/outbound-defaults
```
**Remove the saved outbound agent:**
```
DELETE /v1/numbers/:numberId/outbound-defaults
```
The MCP tools `set_outbound_defaults`, `get_outbound_defaults`, and `clear_outbound_defaults` mirror these endpoints. `list_numbers` and `get_number` responses include an `outbound` field alongside the existing `inbound` field, so a single fetch surfaces both per-number configurations.
### Hydrating from the saved outbound agent and replay-safe retries
`POST /v1/calls/ai` (MCP `initiate_ai_call`) accepts two optional input fields designed for CSV-driven outbound runs.
**`useSavedAgent: boolean`**: when true, AgentCall hydrates the omitted call fields (`systemPrompt`, `voice`, `language`, `firstMessage`, `maxDurationSecs`, `record`) from the saved outbound agent on the `from` number. Per-call values in the same request always win, so the saved persona can be inherited while `firstMessage` is personalized per recipient. If the `from` number has no saved outbound agent, the API returns 400 with code `no_saved_agent`; call `set_outbound_defaults` first or supply the fields inline.
**`idempotencyKey: string`** (1 to 200 chars, scoped per phone number): a duplicate request to `POST /v1/calls/ai` with the same `from` + `idempotencyKey` pair replays the original response and does not place a second carrier call. The replayed response carries an `X-AgentCall-Idempotency-Replayed: true` header. Uniqueness is enforced at the database layer, so concurrent retries are race-safe.
**Canonical CSV-runner pattern** (one row per call, replay-safe across retries, persona stored once on the number):
```
{
"from": "num_xxx",
"to": "+14155551234",
"useSavedAgent": true,
"idempotencyKey": "batch-2026-05-25:row-0042",
"firstMessage": "Hi Jamie, calling to confirm tomorrow's 10am appointment."
}
```
This is the recommended shape for CSV-driven outbound on a Hermes-style runner. The persona lives on the number via `set_outbound_defaults`; each row supplies only the recipient, a per-row `idempotencyKey`, and an optional per-row override.
**List call history:**
```
GET /v1/calls
Query: ?limit=20
```
**Get call details:**
```
GET /v1/calls/:callId
```
**Get AI call transcript:**
```
GET /v1/calls/:callId/transcript
Response: { "entries": [{ "role": "ai" | "human", "text": "...", "timestamp": "..." }], "summary": "...", "duration": 111 }
```
**Hang up an active call:**
```
POST /v1/calls/:callId/hangup
```
## Webhooks
> **Important: webhooks here are for OUTBOUND event delivery, NOT inbound call routing.**
> AgentCall POSTs to your URL when events fire (SMS received, OTP detected, call completed,
> recording ready, etc.). Webhooks are NOT how you make a phone number answer incoming calls.
> To configure how a number answers when called, use the **Inbound AI Voice** section above
> (`POST /v1/numbers/:numberId/inbound-config`). Not webhooks. (If you're coming from Twilio:
> AgentCall replaces Twilio's inbound voice webhook URL with `configure_inbound_ai`.)
**Register a webhook:**
```
POST /v1/webhooks
Body: { "url": "https://example.com/hook", "events": ["sms.inbound", "sms.otp", "call.status"] }
Events: sms.inbound, sms.otp, call.inbound, call.ringing, call.status, call.recording, call.transcript, number.released
```
**List webhooks:**
```
GET /v1/webhooks
```
**Rotate webhook secret:**
```
POST /v1/webhooks/:id/rotate
```
**Delete a webhook:**
```
DELETE /v1/webhooks/:id
```
### Post-call transcript webhook (`call.transcript`)
When the user wants their AI agent to learn from inbound AI calls, subscribe a webhook to the `call.transcript` event. AgentCall POSTs the full transcript and an LLM-extracted summary to the configured URL after each inbound AI call ends.
**Subscribe:**
```
POST /v1/webhooks
Body: { "url": "https://agent.example.com/agentcall/transcript", "events": ["call.transcript"] }
```
**Payload shape:**
```
{
"callId": "call_xxx",
"duration": 53,
"transcript": [
{ "role": "ai" | "human", "text": "...", "timestamp": "ISO 8601" }
],
"summary": {
"summary": "1-2 sentence plain English",
"callerName": "string or null",
"intent": "service_request | quote_request | scheduling | complaint | spam | general_inquiry | other",
"urgency": "high | medium | low",
"callbackBy": "string or null",
"spam": true | false
}
}
```
**Required handling on the receiving endpoint:**
- Verify the HMAC-SHA256 signature in the `X-AgentCall-Signature` header using the webhook's signing secret. Reject mismatches with a non-2xx response.
- Dedup on `callId`. AgentCall retries with exponential backoff until it receives a 2xx, so a slow first response can produce a duplicate delivery.
- Acknowledge fast: return 2xx as soon as the payload is verified and persisted. Heavy processing belongs in a background job, not inline on the request.
**Integration patterns:**
- **Store** the payload to a database row keyed by `callId` and let the agent read it on its own schedule.
- **Forward** the payload to an agent platform's ingest endpoint for real-time follow-up.
- **Queue** the payload on a public bridge for a local agent platform to drain on cron. Reference bridge: github.com/Kintupercy/agentcall-hermes-bridge v0.2.0 exposes `POST /agentcall/transcript` (HMAC-verified queue append, max 100 entries, oldest dropped) and `POST /hermes/pull-transcripts` (`X-Hermes-Push-Key` authed read-and-clear).
Full walkthrough with code examples: https://agentcall.co/docs/post-call-webhook.
## Auditable Call Memory
Every completed AI call can be auto-extracted into structured, source-backed memory: contacts, facts, preferences, commitments, tasks, briefs. Each memory item carries a verbatim transcript quote as evidence and a full audit timeline. Cross-call memory means an inbound or outbound AI call can be aware of what was said in prior calls from the same number, automatically.
Memory is opt-in per agent: set `memoryEnabled: true` on your agent (currently via the dashboard or direct DB update; account-level toggle endpoint is planned). Once enabled, every completed AI call produces a `CallReport` and a set of `MemoryCandidate` rows roughly 5 to 30 seconds after the call ends.
### Contacts
A `Contact` is one caller per phone per agent. Created automatically on the first call from or to a new number when memory is enabled. Contains `displayName`, `tags`, `ownerNotes`, `verified`, `blockedAt`, `callCount`, `lastSeenAt`. Caller ID is NOT verified by AgentCall by default; `verified` is owner-set or future STIR/SHAKEN attestation.
**List contacts:**
```
GET /v1/contacts?limit=20&cursor=...
```
**Look up by phone (E.164):**
```
GET /v1/contacts/by-phone/+14155551234
```
**Edit owner fields:**
```
PATCH /v1/contacts/:id
Body: { displayName?, tags?, ownerNotes?, verified?, blockedAt? }
```
**Right-to-forget** (wipes Memory + MemoryCandidate + Brief; redacts CallReport payloads; preserves Call + transcript audit shell):
```
DELETE /v1/contacts/:id
```
### Get the next-call context block (the killer tool)
Before placing or answering an AI call, an agent can ask AgentCall what is known about the caller. The endpoint returns the compact natural-language paragraph that would be auto-injected into the next AI call's system prompt for that caller.
```
GET /v1/contacts/:id/next-call-context
```
Response:
```json
{
"contactId": "ct_abc",
"phone": "+14155551234",
"numberId": "num_xyz",
"contextBlock": "Caller: Percy Kintu (unverified caller ID)\nHistory: 4 prior calls. Last call on 2026-05-16.\n\nWhat we know about this caller:\n - Caller's first name is Percy\n - Caller's middle name is David\n\nKnown about the caller's company:\n - Caller's product or app name is Wandereel\n\nOpen follow-ups from prior calls:\n - Call back at 6:30 PM Central",
"contextBlockChars": 384,
"isEmpty": false
}
```
Empty block means no useful memory exists yet (first call from this contact). Pair with `get_contact_by_phone` to look up by E.164 if you only have a phone number.
### Inbound AI: pre-call context source
`POST /v1/numbers/:numberId/inbound-config` now accepts `contextSource`:
- `"none"`: no injection (default when neither memory nor webhook is configured)
- `"agentcall_memory"`: render the Memory block from AgentCall's own memory layer and inject. Zero external infrastructure.
- `"webhook"`: call the customer's `contextWebhook` URL (existing pre-call brief flow). Default when `contextWebhook` is set but `contextSource` is not.
- `"merge"`: render the Memory block AND call the customer webhook with the memory block in the payload. The webhook's response extends the Memory block in the final prompt. Recommended for customers already running their own brief pipeline.
### Memory: Current Truth, Candidates, Timeline
Memory is the "Current Truth" layer: at most one active row per concept slot. Candidates are the extractor's proposals; the auto-promotion policy decides which auto-apply.
**Read Current Truth for a contact:**
```
GET /v1/memory/current?contactId=ct_abc
```
**Audit log for one memory row:**
```
GET /v1/memory/:id/timeline
```
**List candidates** (filter by status, contactId, type):
```
GET /v1/memory/candidates?status=proposed&contactId=ct_abc
```
**Owner edits a memory row** (text or expiry; writes an `edited` MemoryEvent):
```
PATCH /v1/memory/:id
Body: { text?, expiresAt? }
```
**Soft-delete a memory row** (sets active=false; writes an `expired` event; survives in DB for audit):
```
DELETE /v1/memory/:id
```
**Manually promote a candidate** (bypasses auto-policy; owner-set always wins contradictions):
```
POST /v1/memory/candidates/:id/approve
Body: { note? }
```
**Manually reject a candidate** (with audit-trail reason):
```
POST /v1/memory/candidates/:id/reject
Body: { reason? }
```
### Bob-vs-Robert handler
When the extractor produces a fact that contradicts an existing Memory row at the same conceptual slot, the auto-policy resolves it as follows:
- New fact's text matches existing -> CORROBORATE: confidence raised, source report appended, no new row.
- New fact's text differs AND new confidence beats old by 0.15 -> SUPERSEDE: existing row deactivated with a `superseded` event, new row created.
- Otherwise -> OLD WINS: new candidate marked `rejected` with the existing memory ID as the contradiction reason. Logged at warn level.
Owner approval via `POST /v1/memory/candidates/:id/approve` always wins contradictions regardless of confidence margin.
### Briefs (owner inbox)
The extractor flags some calls as needing owner attention (hot leads, escalations, AI commitments, missed follow-ups). Those become `Brief` rows.
```
GET /v1/briefs?status=open&urgency=high
POST /v1/briefs/:id/ack
POST /v1/briefs/:id/resolve
```
### Call reports (per call)
Every completed AI call gets one CallReport with the structured analysis:
```
GET /v1/calls/:callId/report
```
Returns 202 with `{ status: "extracting" }` while the worker is still running (typically 5 to 30 seconds). 200 once the report lands. The report payload includes `summary`, `intent`, `urgency`, `spam`, `briefWorthy`, plus arrays of `entities`, `facts`, `preferences`, `decisions`, `commitments`, `tasks`, `risks`, `unresolved`, the `nextCallContext` paragraph, and an `ownerBrief` block if the call needs attention.
**List reports across calls** (filter by contactId, intent, urgency, briefWorthy):
```
GET /v1/reports?contactId=ct_abc&urgency=high
```
### Memory feature webhook event
Subscribe to `call.report.ready` to receive the structured CallReport on every call after the extractor finishes. Distinct from `call.transcript` (which fires earlier, with raw transcript and a one-paragraph summary). Both events fire for the same call when both are subscribed; expect `call.transcript` first within seconds, `call.report.ready` 5 to 30 seconds later.
## Usage & Billing
**Get usage breakdown:**
```
GET /v1/usage
Query: ?period=2026-04
```
Pricing (per use, Pro plan):
- SMS outbound: $0.015/msg
- SMS inbound: $0.008/msg
- Voice (standard outbound): $0.035/min
- Voice (standard inbound): $0.015/min
- AI voice (outbound): $0.40/min
- AI voice (inbound, Managed billing mode): $0.40/min
- AI voice (inbound, BYOK billing mode): $0.10/min
- Call recording: $0.01/min
## Phone Number Format
All phone numbers must be E.164: `+{country code}{number}`, e.g. `+14155551234`
## Common Workflows
### Set up an AI receptionist on a phone number
1. `POST /v1/numbers/provision` with `{ "type": "local" }`: get a number
2. `GET /v1/calls/prompt-templates`: pick a template (e.g. "receptionist")
3. Replace `[BRACKETED]` placeholders with the customer's real business details
4. `POST /v1/numbers/:numberId/inbound-config` with `{ "mode": "ai", "systemPrompt": "...", "voice": "shimmer", "firstMessage": "..." }`
5. Anyone who calls the number is now answered by the AI agent
6. `GET /v1/calls` to see incoming call history; `GET /v1/calls/:callId/transcript` for transcripts
### Test your app's SMS verification (QA)
1. `POST /v1/numbers/provision` with `{ "type": "local" }`: get a test number
2. Enter the number into your staging app's verification form
3. `GET /v1/sms/otp/:numberId?timeout=60000`: wait for the verification code
4. Assert the code arrives and your app accepts it
5. `DELETE /v1/numbers/:id`: release the test number
### Make an outbound AI voice call
1. `POST /v1/numbers/provision` with `{ "type": "local" }`: get a number (if you don't have one)
2. `POST /v1/calls/ai` with `{ "from": "num_xxx", "to": "+1...", "systemPrompt": "..." }`: start the call
3. Wait for the call to complete
4. `GET /v1/calls/:callId/transcript`: get the full conversation transcript
### Look up what AgentCall knows about a caller before calling them (cross-call memory)
1. `GET /v1/contacts/by-phone/+14155551234`: resolve the contact (returns 404 if AgentCall has never seen this number)
2. `GET /v1/contacts/:id/next-call-context`: render the Memory block the AI would receive in its system prompt on the next call to or from this number
3. Decide: place the call, draft a message based on what is known, or escalate to a human. If isEmpty=true, AgentCall has no extracted memory yet and the AI would start with only the static system prompt.
### Switch an inbound AI number from Managed to BYOK billing
1. Confirm the user wants to switch billing modes on the specific number (Managed $0.40/min vs BYOK $0.10/min), has a working AI provider key on hand, and understands the rate change.
2. `POST /v1/numbers/:numberId/byok-key` with `{ "openaiApiKey": "sk-..." }`: stores the customer-provided key on the number and flips it to BYOK billing.
3. `GET /v1/numbers/:numberId/inbound-config`: verify `voiceMode: "byok"`, `hasByokKey: true`, and the redacted preview (`byokOpenaiApiKeyPreview`).
4. To revert later: `DELETE /v1/numbers/:numberId/byok-key`. Reverts to Managed billing; system prompt, voice, recording flag, and notify block are preserved.
### Make an inbound AI receptionist remember every caller
1. Configure inbound AI on the number (see "Set up an AI receptionist on a phone number" workflow above).
2. Enable memory on your agent (`memoryEnabled: true`) via the dashboard.
3. Set `contextSource: "agentcall_memory"` on the inbound-config call: every inbound call now arrives at the AI with prior-call memory injected into the system prompt automatically.
4. Optionally subscribe to `call.report.ready` webhook to receive the structured CallReport after each call.
5. To layer your own dynamic brief on top of AgentCall's memory, set `contextSource: "merge"` and provide a `contextWebhook` URL. AgentCall calls your webhook with its memory block in the payload; your response extends the final prompt.
## Error Codes
- **401**: Invalid or missing API key
- **402 payment_method_required**: Add a card before configuring billable features (returns `setupUrl` to Stripe Checkout)
- **403 plan_limit_voice_ai**: Outbound AI voice requires Pro plan ($19.99/mo).
- **403 plan_limit_inbound_ai_trial_exhausted**: Free user has used up their 5-minute monthly inbound AI trial. Returned with `upgradeUrl`. Trial resets on the 1st of next month (UTC) or the user can upgrade to Pro for unlimited inbound AI.
- **403 plan_limit_***: Other plan limits. Upgrade at agentcall.co/dashboard
- **400 carrier_not_supported**: Inbound AI voice is only supported on US and Canada numbers
- **404**: Resource not found
- **422**: Validation error (check request body)
- **429**: Rate limit exceeded (100 req/min global; per-route limits on expensive endpoints)
- **503 voice_ai_unavailable**: Server-side AI voice infrastructure issue. Retry later
don't have the plugin yet? install it then click "run inline in claude" again.
agentcall gives you a production-grade phone API for ai agents: provision real phone numbers in the US and Canada, send SMS, receive OTP codes, handle inbound calls with ai voice receptionists, and place outbound calls (standard or ai-driven). use this when your agent needs to interact with humans over voice or text, verify identities with one-time passcodes, or run phone-based workflows at scale. several endpoints cost real money (sms $0.015/msg, outbound ai voice $0.40/min, inbound ai voice $0.40/min managed or $0.10/min bring-your-own-key on pro plan). always confirm scope and budget with the user before invoking billable actions.
authentication:
AGENTCALL_API_KEY environment variable. all authenticated requests require Authorization: Bearer <AGENTCALL_API_KEY> header.https://api.agentcall.coGET https://api.agentcall.co/llms.txtrate limits:
carrier restrictions:
carrier_not_supported for other countries.plan-specific gating:
trial_exhausted. trial resets 1st of month utc or when user upgrades to pro.setupUrl if card is missing.two-party-consent disclosure:
record: true on ai voice paths, agentcall auto-prepends "this call may be recorded for quality." to spoken firstMessage if not already mentionedfirstMessage content when user writes one from scratchwebhooks (outbound event delivery only, not inbound routing):
POST /v1/webhooks to receive events: sms.inbound, sms.otp, call.inbound, call.ringing, call.status, call.recording, call.transcript, number.releasedPOST /v1/numbers/:id/inbound-config (see procedure step 2), not via webhook urllist existing numbers:
GET /v1/numbers?limit=20&country=US&type=local
Response: { numbers: [{id, number, type, country, label, inbound, outbound, ...}], ...}
provision a new number (billable: $2/mo local/mobile, $2.50/mo tollfree; monthly recurring): before invoking, confirm with user: exact type (local/tollfree/mobile), country (us/ca), and label for tracking.
POST /v1/numbers/provision
Body: { "type": "local", "country": "US", "label": "my-agent" }
Response: { "id": "num_xxx", "number": "+12125551234", "type": "local", ...}
numbers are voip-routed via licensed us carriers. not suitable for consumer platform signup verification (stripe, whatsapp, google, banks).
get number details (including current inbound and outbound config):
GET /v1/numbers/:id
Response: { id, number, type, country, label, inbound: {...}, outbound: {...}, ... }
rename a number or update voice/language (preserves all other inbound config):
PATCH /v1/numbers/:id
Body: { "label": "Hermes assistant" } // rename
Body: { "voice": "marin" } // voice only (requires inbound ai already configured)
Body: { "language": "es" } // language only (requires inbound ai already configured)
Body: { "label": "Hermes", "voice": "coral", "language": "fr" } // any combo
Voices: alloy, ash, ballad, cedar, coral, echo, marin, sage, shimmer, verse
Languages: auto, en, es, fr, de, it, pt, nl, ja, ko, zh, hi, ar
Response: full number object with updated inbound block
use patch instead of calling POST /v1/numbers/:id/inbound-config for voice/language changes; that endpoint replaces entire inbound config and wipes system prompt, first message, recording flag, notify block.
release a number (irreversible; billable recurring charge stops): before invoking, warn user this is permanent and the same number cannot be re-provisioned.
DELETE /v1/numbers/:id
pre-flight checklist (require explicit user confirmation for each before invoking):
record: true or false). default false. confirm explicit opt-in for recording. mention $0.01/min delta.notify.emailTo). confirm address belongs to user and they want post-call summaries.maxDurationSecs). pick smallest value fitting use case instead of relying on 600s default.configure inbound ai (billable on pro: $0.40/min managed or $0.10/min byok; free trial 5 min/month):
POST /v1/numbers/:numberId/inbound-config
Body: {
"mode": "ai",
"systemPrompt": "You are the front desk for Acme Plumbing. Greet warmly, take name and brief issue description, say someone will call back within 24 hours.",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, thanks for calling Acme Plumbing. How can I help?",
"maxDurationSecs": 300,
"record": false,
"notify": {
"emailTo": "owner@acmeplumbing.com",
"businessName": "Acme Plumbing",
"agencyName": "Acme Plumbing"
},
"contextWebhook": { // optional
"url": "https://your-domain.com/agentcall/precall",
"signingSecret": "<32+ char secret>",
"timeoutMs": 1200
}
}
Response: { id, systemPrompt, voice, language, firstMessage, maxDurationSecs, record, notify, voiceMode, hasByokKey, byokOpenaiApiKeyPreview, ... }
spoken language behavior:
language: "auto" (default): ai matches caller's language naturallylanguage: "es" (specific iso-639-1): ai responds only in that language even if caller speaks another. useful for language-specific business communities. directive added at session-build time; customer's system prompt stays unchanged.context webhook (optional live context injection):
agentcall posts to your https url on every inbound call connect (hmac-signed with signingSecret). your endpoint responds with {"contextBlock":"..."} and agentcall merges that onto system prompt before ai answers. injects today's brief, priorities, or recent signals so ai speaks with up-to-date data instead of static prompt. fail-open: webhook errors leave call running with static prompt. see https://agentcall.co/docs/hermes for walkthrough.
post-configuration responsibility:
GET /v1/usage or check dashboard at /billingDELETE /v1/numbers/:numberId/inbound-config when testing done, campaign over, or season closedget current inbound config:
GET /v1/numbers/:numberId/inbound-config
Response: { mode, systemPrompt, voice, language, firstMessage, maxDurationSecs, record, notify, voiceMode, hasByokKey, byokOpenaiApiKeyPreview, ... }
disable inbound ai (calls hang up at carrier):
DELETE /v1/numbers/:numberId/inbound-config
switch to byok ($0.10/min, customer provides openai key): before invoking, confirm user wants to switch billing modes on specific number, has working key on hand, understands per-minute rate change.
POST /v1/numbers/:numberId/byok-key
Body: { "openaiApiKey": "sk-..." }
Response: { "voiceMode": "byok", "hasByokKey": true, "byokOpenaiApiKeyPreview": "sk-...abcd" }
system prompt, voice, recording flag, notify block preserved across switch. stored key is write-only; get response shows redacted preview (last 4 chars) and boolean, never full value. rotate by re-posting new key; agentcall replaces atomically.
switch back to managed ($0.40/min, agentcall holds key): before invoking, confirm user wants to revert billing modes.
DELETE /v1/numbers/:numberId/byok-key
Response: { "voiceMode": "managed", "hasByokKey": false, "byokOpenaiApiKeyPreview": null }
system prompt, voice, recording flag, notify block preserved.
send sms (billable: $0.015/msg on pro, 10/month free on free plan): before invoking, confirm recipient number and message body.
POST /v1/sms/send
Body: { "from": "num_xxx", "to": "+14155551234", "body": "Hello!" }
"from" can be number id or e.164 phone string
get sms inbox:
GET /v1/sms/inbox/:numberId?limit=20&otpOnly=true
get specific sms message:
GET /v1/sms/:messageId
wait for otp code (long-polls up to 60 sec): useful for two-factor flows where agent needs to extract and use a code from inbound sms.
GET /v1/sms/otp/:numberId?timeout=60000
Response: { "otp": "482913", "message": { ... } }
start standard outbound call:
POST /v1/calls/initiate
Body: { "from": "num_xxx", "to": "+14155551234", "record": false }
start outbound ai voice call (billable: $0.40/min on pro, contacts real human, confirm user first): ai handles entire conversation autonomously based on system prompt. before invoking, confirm destination number and purpose.
POST /v1/calls/ai
Body: {
"from": "num_xxx",
"to": "+14155551234",
"systemPrompt": "You are calling to schedule a dentist appointment for Tuesday afternoon.",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, I'd like to schedule an appointment please.",
"maxDurationSecs": 600,
"record": false,
"useSavedAgent": false,
"idempotencyKey": "batch-2026-05-25:row-0042"
}
Response: { "id": "call_xxx", "from", "to", "status", "transcript", "duration", ... }
optional recording on ai call (adds $0.01/min):
add record: true to capture mp3. mention recording cost delta when proposing to user. recordings live in dashboard for 1 year, listenable from logs > calls tab or via GET /v1/calls/:callId/recording (returns fresh short-lived signed url). call.recording webhook fires when ready.
spoken language on outbound calls (optional, default auto):
language: "auto" lets ai match recipient's language naturally. pass iso-639-1 code (e.g. language: "es") to make ai respond only in that language. useful when booking appointments on behalf of non-native speaker (e.g. calling spanish-speaking doctor's office on behalf of english-speaking customer). firstMessage stays verbatim; if you want greeting in spanish, write firstMessage in spanish yourself.
csv-driven outbound with saved agent (replay-safe):
save outbound defaults on number once (step 6 below). then use useSavedAgent: true and personalize only firstMessage per row:
{
"from": "num_xxx",
"to": "+14155551234",
"useSavedAgent": true,
"idempotencyKey": "batch-2026-05-25:row-0042",
"firstMessage": "Hi Jamie, calling to confirm tomorrow's 10am appointment."
}
idempotencyKey scoped per phone number ensures duplicate request with same from + key replays original response without placing second carrier call. replayed response carries X-AgentCall-Idempotency-Replayed: true header. uniqueness enforced at db layer; concurrent retries race-safe.
get call history:
GET /v1/calls?limit=20
get call details:
GET /v1/calls/:callId
Response: { id, from, to, status, duration, transcript, recording, ... }
get ai call transcript:
GET /v1/calls/:callId/transcript
Response: { "entries": [{ "role": "ai" | "human", "text": "...", "timestamp": "..." }], "summary": "...", "duration": 111 }
hang up active call (irreversible; confirm if call still progressing toward goal):
POST /v1/calls/:callId/hangup
save outbound agent defaults on a number (optional; useful for repeated calls):
POST /v1/numbers/:numberId/outbound-defaults
Body: {
"systemPrompt": "You are calling on behalf of Acme Plumbing to confirm tomorrow's 10am appointment.",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, calling to confirm tomorrow's appointment.",
"maxDurationSecs": 300,
"record": false,
"templateId": "outbound-callback-confirmation"
}
read saved outbound agent (returns null if none set):
GET /v1/numbers/:numberId/outbound-defaults
Response: { systemPrompt, voice, language, firstMessage, maxDurationSecs, record, templateId }
remove saved outbound agent:
DELETE /v1/numbers/:numberId/outbound-defaults
list_numbers and get_number responses include outbound field alongside inbound, so single fetch surfaces both per-number configurations. independent of inbound ai receptionist; configuring one does not change the other.
list 10 voices with samples:
GET /v1/calls/voices
Response: { voices: [{ id, name, trait, description, bestFor, sampleUrl? }, ...], defaultVoice: "shimmer" }
newest top picks: marin (soft, natural) and cedar (warm, grounded). original 8 (shimmer, sage, ash, ballad, coral, echo, verse, alloy) ship with sample mp3s; marin and cedar live-preview only for now.
list prompt templates:
GET /v1/calls/prompt-templates?direction=inbound
Returns 5 inbound templates: receptionist (front desk, shimmer), lead-qualifier (sales bant, coral), appointment-booker (sage), customer-support (faq deflection, ash), call-screener (anti-spam, verse).
Each: id, title, description, recommendedVoice, maxDurationSecs, firstMessage, systemPrompt with [BRACKETED] placeholders.
GET /v1/calls/prompt-templates?direction=outbound
Returns 3 outbound templates: outbound-appointment-booker, outbound-info-request, outbound-callback-confirmation.
Same structure; fill in placeholders before use.
full prompt-writing guide at https://agentcall.co/docs/voice-prompts
note: webhooks deliver events (sms received, call completed, recording ready, etc.). inbound call routing is configured via POST /v1/numbers/:id/inbound-config, not webhook url.
register webhook:
POST /v1/webhooks
Body: { "url": "https://example.com/hook", "events": ["sms.inbound", "sms.otp", "call.status", "call.recording", "call.transcript", "number.released"] }
list webhooks:
GET /v1/webhooks
rotate webhook secret:
POST /v1/webhooks/:id/rotate
delete webhook:
DELETE /v1/webhooks/:id
post-call transcript webhook (call.transcript):
when agent needs to learn from inbound ai calls, subscribe to call.transcript event. agentcall posts full transcript and llm-extracted summary to your endpoint.
user requests to provision a number:
POST /v1/numbers/provision and surface the new number id and e.164 string.user requests to send sms:
POST /v1/sms/send and confirm success.user requests inbound ai voice configuration:
user requests outbound ai call:
user reports unexpected calls or charges:
DELETE /v1/numbers/:numberId/inbound-config to stop further charges.GET /v1/calls and GET /v1/calls/:callId/transcript to understand what happened.GET /v1/webhooks) to see if any external events are triggering unwanted calls.user requests voice mode switch (managed to byok or vice versa):
user requests release of a number:
DELETE /v1/numbers/:id after explicit confirmation.user is on free plan and would exceed any hard cap (1 number, 10 sms/month, 5 voice min/month, 5 inbound ai min/month, 5 otp extractions/month):
user wants to use csv-driven outbound calling at scale:
POST /v1/numbers/:numberId/outbound-defaults.useSavedAgent: true and idempotencyKey per row for replay-safe retries.idempotencyKey on different from numbers will both fire; use unique keys or include the number id in the key string.api returns 402 payment method required:
setupUrl from response so user can add card directly.api returns 403 plan limit inbound ai trial exhausted:
upgradeUrl from response.api returns 400 carrier not supported:
POST /v1/calls/initiate instead if user needs non-us/ca number.api returns 400 no saved agent:
from number has no saved outbound agent.POST /v1/numbers/:numberId/outbound-defaults first, or supply fields inline in the call request.api rate limit hit (100 req/min global, 10 req/min ai voice, 20 req/hour uploads):
provisioned number:
{
"id": "num_xxx",
"number": "+12125551234",
"type": "local",
"country": "US",
"label": "my-agent",
"inbound": null,
"outbound": null,
"createdAt": "2026-01-15T10:30:00Z"
}
inbound ai configuration response:
{
"mode": "ai",
"systemPrompt": "You are the front desk for Acme Plumbing...",
"voice": "shimmer",
"language": "auto",
"firstMessage": "Hi, thanks for calling Acme Plumbing. How can I help?",
"maxDurationSecs": 300,
"record": false,
"notify": {
"emailTo": "owner@acmeplumbing.com",
"businessName": "Acme Plumbing",
"agencyName": "Acme Plumbing"
},
"voiceMode": "managed",
"hasByokKey": false,
"byokOpenaiApiKeyPreview": null,
"contextWebhook": {
"url": "https://your-domain.com/agentcall/precall",
"signingSecret": "<redacted>",
"timeoutMs": 1200
},
"createdAt": "2026-01-15T10:30:00Z"
}
sms send response:
{
"id": "sms_xxx",
"from": "num_xxx",
"to": "+14155551234",
"body": "Hello!",
"status": "queued",
"sentAt": "2026-01-15T10:30:00Z"
}
otp extraction response:
{
"otp": "482913",
"message": {
"id": "sms_xxx",
"from": "+1234567890",
"to": "+14155551234",
"body": "Your code is 482913",
"receivedAt": "2026-01-15T10:30:15Z"
}
}