# Outbound Calls

> Originate a PSTN call over a SIP trunk, attach an AI agent, cap the ring time, and poll for the outcome.

Place a call to any phone number over one of your SIP trunks with a single
control-plane call. You supply the destination, a caller-id, and the trunk to
route over. The engine dials the carrier. You get back a `Call` handle. Its
`sid` addresses everything else: audio, transfer, hangup, telemetry.

> **NOTE:**
> Outbound originate is a **control-plane** operation. It returns as soon as
>   the call is `dialing`, not when the callee answers. To read the outcome, poll
>   `calls.get` (below), or consume
>   [call-lifecycle events](/modalities/voice/transport-telephony/call-lifecycle).

## Before you start

  1. **A configured SIP trunk**
Outbound calls route over a trunk that you own. Create one and note its
    `trunkId` — see [SIP trunking](/modalities/voice/transport-telephony/sip-trunking).
  2. **A caller-id that you are allowed to present**
The `from` number must be one that the trunk (or your carrier) permits. The
    numbers that you allocated live in
    [number provisioning](/modalities/voice/transport-telephony/number-provisioning).
  3. **An initialized Voice client**
```ts
    import { Voice } from "clutchcall_connect/voice";

    const v = new Voice({
      baseUrl: "https://engine.firstbyte.fast",
      apiKey:  process.env.CLUTCHCALL_CREDENTIALS!,
      orgId:   "org_abc",
    });
    ```

## Place a call

Call `calls.originate` with the destination, caller-id, and trunk. The returned
handle carries the `sid`. The `sid` is the only identifier that you need for
everything that follows.

  
```ts
const call = await v.calls.originate({
  to:      "+15551234567",   // E.164 destination
  from:    "+15558675309",   // caller-id you're allowed to present
  trunkId: "trunk_main",     // SIP trunk to route over
});

console.log(call.sid, call.status); // e.g. "call_9f2c…" "dialing"
```
  
  
```python
import os
from clutchcall.voice import Voice

v = Voice(
    base_url="https://engine.firstbyte.fast",
    api_key=os.environ["CLUTCHCALL_CREDENTIALS"],
    org_id="org_abc",
)

call = v.calls.originate(
    to="+15551234567",
    from_="+15558675309",
    trunk_id="trunk_main",
)

print(call.sid, call.status)  # e.g. "call_9f2c…" "dialing"
```
  

- **`to`** (`string`, required) — The E.164 destination number.
- **`from`** (`string`, required) — The caller-id, in E.164 form. The trunk must permit it.
- **`trunkId`** (`string`, required) — The SIP trunk to route the call over.
- **`agent`** (`string`) — The AI agent id to attach automatically on answer (see below).
- **`ringTimeoutSec`** (`number`) — The seconds to ring before the engine gives up. The server clamps the value   to **5..120**; the default is 30.

## Attach an AI agent

Pass `agent`, and the engine wires the audio bridge for you the moment the
callee answers. The agent starts talking with no `AudioBridge` code on your
side. This one call is the whole outbound-AI path.

  
```ts
const call = await v.calls.originate({
  to:      "+15551234567",
  from:    "+15558675309",
  trunkId: "trunk_main",
  agent:   "appointment-reminder",   // attaches on answer
});
```
  
  
```python
call = v.calls.originate(
    to="+15551234567",
    from_="+15558675309",
    trunk_id="trunk_main",
    agent="appointment-reminder",   # attaches on answer
)
```
  

> **TIP:**
> The `agent` id names a server-side agent that you configured: its
>   speech-to-speech or ASR/LLM/TTS pipeline, prompt, and tools. Build one in the
>   [agent runtime](/modalities/voice/runtime/overview). To attach an agent to a
>   call that is **already** connected, or to hand a call between a bot and a
>   person, see
>   [AI ↔ human handoff](/modalities/voice/transport-telephony/ai-human-handoff).

You can also own the media yourself: feed the caller's audio to your own
recognizer, and push your own synthesized audio back. To do this, omit `agent`
and attach an [audio bridge](/modalities/voice/sdk/javascript) to the `sid`
instead.

## Cap the ring time

`ringTimeoutSec` bounds how long the callee's phone rings before the engine
abandons the call as `no_answer`. Pass a shorter value for tight retry loops.
Pass a longer value for numbers that are slow to pick up. The engine **clamps
the value to 5..120 seconds**. A value below 5 becomes 5. A value above 120
becomes 120. You never have to validate the value yourself.

  
```ts
const call = await v.calls.originate({
  to:      "+15551234567",
  from:    "+15558675309",
  trunkId: "trunk_main",
  ringTimeoutSec: 15,   // give up after 15s instead of the default 30
});
```
  
  
```python
call = v.calls.originate(
    to="+15551234567",
    from_="+15558675309",
    trunk_id="trunk_main",
    ring_timeout_sec=15,   # give up after 15s instead of the default 30
)
```
  

> **NOTE:**
> `ringTimeoutSec: 3` is accepted but behaves as `5`. `ringTimeoutSec: 240`
>   behaves as `120`. The clamp is silent. The value that you read back on the
>   call can differ from the value that you sent.

## Poll for the outcome

`originate` returns at `dialing`. To learn whether the call connected, fetch
the call again. `calls.get({ sid })` returns a fresh `Call` with the current
`status`.

  
```ts
const c = await v.calls.get({ sid: call.sid });
if (c.status === "in_progress") console.log("connected");
```
  
  
```python
c = v.calls.get(sid=call.sid)
if c.status == "in_progress":
    print("connected")
```
  

A call moves through these statuses:

| Status        | Meaning                                              |
| ------------- | ---------------------------------------------------- |
| `dialing`     | Originate is accepted. The engine reaches out to the carrier. |
| `ringing`     | The callee's phone is ringing.                       |
| `in_progress` | The call is answered and connected. Audio is flowing. |
| `completed`   | The call ended normally after it connected.          |
| `no_answer`   | The call rang past `ringTimeoutSec` without an answer. |
| `failed`      | The call was rejected, busy, or unreachable.         |

Poll until the call leaves the ringing states to get its terminal outcome:

  
```ts
async function waitConnected(sid: string) {
  for (;;) {
    const c = await v.calls.get({ sid });
    if (c.status !== "dialing" && c.status !== "ringing") return c.status;
    await new Promise((r) => setTimeout(r, 500));
  }
}

const outcome = await waitConnected(call.sid); // in_progress | no_answer | failed
```
  
  
```python
import time

def wait_connected(sid: str) -> str:
    while True:
        c = v.calls.get(sid=sid)
        if c.status not in ("dialing", "ringing"):
            return c.status
        time.sleep(0.5)

outcome = wait_connected(call.sid)  # in_progress | no_answer | failed
```
  

> **TIP:**
> Polling is fine for one-off calls. For fleets of calls, subscribe to
>   [call-lifecycle events](/modalities/voice/transport-telephony/call-lifecycle)
>   instead of polling each `sid`. The engine pushes every transition
>   (`initiated`, `ringing`, answered, `cleared`) as it happens.

## Placing many calls

`calls.originate` places one call. For high-volume outbound — pacing thousands
of numbers with a rate limit and a concurrency cap — use the control-plane bulk
dialer. Do not loop `originate` yourself. The bulk dialer owns the pacing and
back-pressure. See the
[PBX & ACD guide](/modalities/voice/transport-telephony/pbx-acd) for campaign
setup.

## Related

  - **[SIP trunking](/modalities/voice/transport-telephony/sip-trunking)** — Create and configure the trunks that outbound calls route over.
  - **[Number provisioning](/modalities/voice/transport-telephony/number-provisioning)** — Allocate the numbers that you present as caller-id.
  - **[Call lifecycle](/modalities/voice/transport-telephony/call-lifecycle)** — The full event stream — a push alternative to polling status.
  - **[AI ↔ human handoff](/modalities/voice/transport-telephony/ai-human-handoff)** — Attach an agent mid-call, or hand a call between a bot and a person.
