---
title: "Placing calls"
description: "Have an agent call someone — Aid Voice handles the call with no server to run, or your own agent talks — and decide what happens when voicemail answers."
canonical_url: "https://www.agent-identity.dev/docs/phone/placing-calls"
markdown_url: "https://www.agent-identity.dev/docs/phone/placing-calls.md"
last_updated: "2026-10-09"
x_farming_labs_generated_preamble: true
agent:
  tokenBudget: 1000
  task: "Place an outgoing call from an agent, with Aid Voice and a task or streamed to your own agent."
  outcome: "POST /v1/identities/{id}/calls returns 201 with the call and the organization's 24-hour usage; the call ends with a call.ended event."
  appliesTo:
    package:
      - "@aid/api"
      - "@agentidentity/sdk"
  prerequisites:
    - "The agent has an active phone number."
    - "The organization's owner is verified."
    - "For your own agent: a wss:// server, given in the request or saved on the agent."
  files:
    - "apps/api/src/services/phone.ts"
  sideEffects:
    - "Dials a real phone and counts against the organization's daily call and minute limits."
  verification:
    - description: "Read the call back."
      expect: "GET /v1/calls/{id} returns the call; status becomes answered and then ended."
  rollback:
    - "POST /v1/calls/{id}/hangup ends a live call."
  failureModes:
    - symptom: "400: reason is required when mode is hosted_agent."
      resolution: "Aid Voice needs a task. Describe what the call is for in reason."
    - symptom: "400: on_voicemail leave_message needs mode hosted_agent."
      resolution: "Only Aid Voice can leave a message. Use hang_up or ignore with your own agent."
    - symptom: "403 destination_country_not_enabled."
      resolution: "Calls are limited to the country codes this server allows (by default +1)."
    - symptom: "429 phone.limit_exceeded with Retry-After."
      resolution: "A daily call or minute limit, or the limit on calls live at once, is reached. Wait Retry-After seconds."
---

# Placing calls
URL: /docs/phone/placing-calls
LLM index: /llms.txt
Description: Have an agent call someone — Aid Voice handles the call with no server to run, or your own agent talks — and decide what happens when voicemail answers.
Related: /docs/phone/aid-voice, /docs/phone/your-agent, /docs/phone/calls-and-events, /docs/phone/limits

<!-- farming-labs:agent-contract:start -->
## Agent Contract

Task: Place an outgoing call from an agent, with Aid Voice and a task or streamed to your own agent.
Outcome: POST /v1/identities/{id}/calls returns 201 with the call and the organization's 24-hour usage; the call ends with a call.ended event.

### Applies To

- Package: `@aid/api`, `@agentidentity/sdk`

### Prerequisites

- The agent has an active phone number.
- The organization's owner is verified.
- For your own agent: a wss:// server, given in the request or saved on the agent.

### Files

- `apps/api/src/services/phone.ts`

### Side Effects

- Dials a real phone and counts against the organization's daily call and minute limits.

### Verification

- Read the call back.
  - Expected: GET /v1/calls/{id} returns the call; status becomes answered and then ended.

### Rollback

- POST /v1/calls/{id}/hangup ends a live call.

### Failure Modes

- 400: reason is required when mode is hosted_agent. — Recovery: Aid Voice needs a task. Describe what the call is for in reason.
- 400: on_voicemail leave_message needs mode hosted_agent. — Recovery: Only Aid Voice can leave a message. Use hang_up or ignore with your own agent.
- 403 destination_country_not_enabled. — Recovery: Calls are limited to the country codes this server allows (by default +1).
- 429 phone.limit_exceeded with Retry-After. — Recovery: A daily call or minute limit, or the limit on calls live at once, is reached. Wait Retry-After seconds.
<!-- farming-labs:agent-contract:end -->

# Placing calls

`POST /v1/identities/{id}/calls` dials a number from the agent's own number. Choose who runs the call with `mode`.

## With Aid Voice, no server needed

Give Aid Voice a task in `reason`. It introduces itself, works the task, records [follow-ups](/docs/phone/aid-voice),
and ends the call politely. No server needed.

```ts title="aid-voice-call.ts"
const call = await org.phone.placeCall({
  identityId: agent.id,
  to: "+14155550100",
  mode: "hosted_agent",
  reason: "Call the dental office and book a cleaning next week, mornings preferred.",
});
```

```bash title="terminal"
curl -sS -X POST "https://api.agent-identity.dev/v1/identities/$AID_AGENT/calls" \
  -H "authorization: Bearer $AID_KEY" -H 'content-type: application/json' \
  -H "idempotency-key: dentist-booking-0412" \
  -d '{
    "to": "+14155550100",
    "mode": "hosted_agent",
    "reason": "Call the dental office and book a cleaning next week, mornings preferred."
  }'
```

`reason` is up to 2,000 characters. If it conflicts with the agent's standing instructions, the task wins.

## With your agent

Leave `mode` out (or send `client_websocket_url`) and the call streams to your agent, exactly like an incoming call. See
[Your agent](/docs/phone/your-agent).

```ts title="own-agent-call.ts"
const call = await self.phone.placeCall({
  identityId: agent.id,
  to: "+14155550100",
  clientWebsocketUrl: "wss://agent.example.com/stream", // or the one saved on the agent
});
```

Without `mode`, a call with a `reason` runs on Aid Voice, and a call with no URL anywhere runs on Aid Voice if that is
the agent's incoming setting.

## When voicemail answers

Every call rings for up to 60 seconds. Unless you turn it off, we listen for an answering machine and its beep.

| `on_voicemail` | What happens | Default for |
| --- | --- | --- |
| `leave_message` | Aid Voice waits for the beep, says `voicemail_message`, then ends the call. | Aid Voice |
| `hang_up` | The call ends at the beep. A greeting or phone menu on its own does not end it. | Your agent |
| `ignore` | No detection. Whatever answers is treated as a person. | |

```ts title="voicemail.ts"
await org.phone.placeCall({
  identityId: agent.id,
  to: "+14155550123",
  mode: "hosted_agent",
  reason: "Confirm tomorrow's 9:30am appointment.",
  onVoicemail: "leave_message",
  voicemailMessage: "Hi, this is Blue Harbor Dental confirming your 9:30am appointment tomorrow. Call us back if you need to reschedule.",
});
```

`voicemail_message` is up to 1,000 characters. Leave it out and a short message is written from `reason`, with the
agent's name and number. While a machine is talking, Aid Voice stays quiet; it speaks again if no beep comes. A call
ended this way has `hangup_reason: "voicemail"`.

## Rules the request is checked against

| Rule | Error |
| --- | --- |
| `mode: "hosted_agent"` needs a `reason`, and no `client_websocket_url`. | 400 |
| `reason` only goes with Aid Voice. | 400 |
| `leave_message` only goes with Aid Voice; `voicemail_message` only with `leave_message`. | 400 |
| `to` is E.164 and is not the agent's own number. `from`, if given, is one of the agent's numbers. | 400 |
| The organization's owner is verified. | 403 `org.not_verified` |
| The destination country is allowed (default `+1`). | 403 `destination_country_not_enabled` |
| The number is not blocked by a rule. | 403 |
| Daily calls and minutes, and calls live at once, are within the limits. | 429 with `Retry-After` |
| The carrier accepts the call. | 502 `phone.provider_unavailable` |

## The response

`201` with the call and `rate_limit`, the organization's use over the last 24 hours:

```json title="response"
{
  "id": "0193…",
  "direction": "outgoing",
  "status": "initiated",
  "mode": "hosted_agent",
  "reason": "Call the dental office and book a cleaning next week, mornings preferred.",
  "on_voicemail": "leave_message",
  "follow_ups": [],
  "rate_limit": { "calls_used": 5, "calls_remaining": 95, "calls_limit": 100, "minutes_used": 14.2, "minutes_remaining": 285.8, "minutes_limit": 300 }
}
```

Send an `Idempotency-Key` header and repeating the request places one call. The outcome arrives as a `call.ended`
event; see [Calls and events](/docs/phone/calls-and-events). `POST /v1/calls/{id}/hangup` (`hangupCall`) ends a live
call from outside.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
