Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# Hosted call agent
description: Let Inkbox answer and place your agent's phone calls with zero setup — no WebSocket server, no webhook required, and a structured post-call package when the call ends

---


# Hosted call agent

Handling a phone call normally means running a WebSocket server: Inkbox dials or answers, connects to your `client_websocket_url`, and streams audio both ways (see [Media Stream](/docs/api/phone/media-stream)). The **hosted call agent** removes the socket entirely. Inkbox runs an opinionated realtime voice agent on the platform side — it answers (or places) the call, knows who it's talking to, can read your agent's full history with that one person, can text or email them mid-call, records action items, and hands your agent a structured package when the call ends.

**Zero setup:** provision a number, flip one setting, and the number answers. No code, no socket, and no webhook required — the [`call.ended` webhook](/docs/webhooks#subscribing-to-call-lifecycle-events) is optional output, never required input.

There are exactly two ways to put a brain on a call:

- **Hosted call agent** — zero setup, run by Inkbox, opinionated, with a fixed built-in toolset. This page.
- **Bring your own** — full control over the audio via `client_websocket_url` and the [media stream](/docs/api/phone/media-stream). Everything documented elsewhere in the Phone docs.

Pick per call (outbound), or per identity or per number (inbound). The two tiers never mix on a single call.

The hosted-agent surface is available in the API, the SDKs, and the CLI from **SDK 0.4.22**.

## Answering calls

Set the identity's [incoming-call action](/docs/api/phone/incoming-call-action) to `hosted_agent`. It's the only answering action with zero prerequisites — no WebSocket URL, no webhook URL:

**Python**

```python
identity.set_incoming_call_action(incoming_call_action="hosted_agent")
```

**TypeScript**

```typescript
await identity.setIncomingCallAction({ incomingCallAction: "hosted_agent" });
```

**CLI**

```bash
inkbox phone incoming-action hosted_agent -i my-agent
```

The same value is accepted when configuring a single number (`PATCH /phone/numbers/{phone_number_id}`, or `inkbox number update --incoming-call-action hosted_agent`). The number-level `PATCH` merges: any stored `client_websocket_url` or `incoming_call_webhook_url` is kept but ignored while the action is `hosted_agent`. The identity-level action set (the snippet above) **replaces the whole inbound-call config** — omitted URLs are cleared, so if you later switch back to `auto_accept` or `webhook`, supply the URL again in that call.

Inbound protections run before the agent picks up: [contact rules](/docs/api/phone/contact-rules) and usage quotas apply exactly as they do for every other action — the hosted agent only answers calls that would have been deliverable anyway.

## Placing calls

Give the agent an errand: place a call with `mode="hosted_agent"` and a plain-language `reason` describing what the call is for.

**Python**

```python
call = identity.place_call(
    to_number="+15551234567",
    mode="hosted_agent",
    reason="Call the dental office and book a cleaning next week, mornings preferred.",
)
print(call.id, call.mode, call.status)
```

**TypeScript**

```typescript
const call = await identity.placeCall({
    toNumber: "+15551234567",
    mode: "hosted_agent",
    reason: "Call the dental office and book a cleaning next week, mornings preferred.",
});
console.log(call.id, call.mode, call.status);
```

**CLI**

```bash
inkbox phone call -i my-agent \\
  --to +15551234567 \\
  --hosted \\
  --reason "Call the dental office and book a cleaning next week, mornings preferred."
```

The `reason` becomes the agent's task brief for the call: it introduces itself, works the errand, records what happened as post-call action items, and ends the call politely. A failed errand is never silent — whatever happens, you get the [post-call package](#the-post-call-package).

Two shape rules, enforced with a `422`: `mode="hosted_agent"` requires a non-empty `reason` (up to 2,000 characters) and must not carry a `client_websocket_url`; `reason` is only valid with `mode="hosted_agent"`. Everything else about placing a call — origination, caller ID rules, rate limits — is unchanged; see the [place call reference](/docs/api/phone/calls#place-call). Two `503`s are possible on hosted calls only: `hosted_agent_unavailable` when the hosted call agent isn't available, and `hosted_agent_at_capacity` when hosted capacity is momentarily saturated — retry the latter shortly.

`mode` works with any `origination`, so the hosted agent can call over a dedicated number or the identity's iMessage line alike.

## Configuring the agent

Each identity carries an optional hosted-agent configuration: a `voice`, a `model`, and free-form `instructions` (up to 8,000 characters) that steer how the agent behaves on that identity's calls. All three are nullable — a `null` field means the platform default applies, and the defaults are sensible, so most identities need no configuration at all.

**Python**

```python
# Read the current config
config = identity.get_hosted_agent_config()
print(config.voice, config.model, config.instructions)

# Replace it (full-replace: omitted fields reset to the platform default)
identity.set_hosted_agent_config(
    instructions="You answer for Blue Harbor Dental. Be warm and brief. "
    "Never quote prices; offer to text the pricing page instead.",
)
```

**TypeScript**

```typescript
// Read the current config
const config = await identity.getHostedAgentConfig();
console.log(config.voice, config.model, config.instructions);

// Replace it (full-replace: omitted fields reset to the platform default)
await identity.setHostedAgentConfig({
    instructions:
        "You answer for Blue Harbor Dental. Be warm and brief. " +
        "Never quote prices; offer to text the pricing page instead.",
});
```

**CLI**

```bash
# Read the current config
inkbox phone hosted-agent get -i my-agent

# Replace it (full-replace: omitted flags reset to the platform default)
inkbox phone hosted-agent set -i my-agent \\
  --instructions "You answer for Blue Harbor Dental. Be warm and brief."
```

Under the hood this is `GET`/`PUT /phone/hosted-agent-config`. Writes are **full-replace**: every omitted or `null` field resets to the platform default. Identity-scoped API keys resolve their own identity; with an admin API key, or when managing from the [Inkbox Console](https://inkbox.ai/console), pass `agent_identity_id`.

Custom instructions are layered on top of everything the agent already knows, so they can steer tone, boundaries, and priorities without you restating the basics. The agent treats them as standing orders from you, applied on every call — with one carve-out: on an outbound errand, the call's `reason` takes precedence if the two conflict, so a standing quirk can't hijack the task the call was placed for.

## What the agent knows

Every hosted session is briefed at answer time — no lookups required from your side:

- **Its own identity** — the agent's display name, handle, email address, and which line the call is on.
- **Who it's talking to** — the counterparty's [contact card](/docs/api/contacts) (name, company, notes, known numbers and emails), resolved by the number on the call. Unknown callers work fine; the agent simply greets them neutrally.
- **Your history with them** — per-channel interaction counts across calls, texts, iMessage, and email, including activity in the last 24 hours and the most recent interaction on each channel. The agent checks history before asking things the caller may have already told you.
- **Their connection state** — whether the caller is opted in to SMS, and whether they're connected over iMessage — so the agent offers follow-ups on channels that will actually work, and can walk them through connecting where they aren't.
- **The local time** — the call clock runs in the counterparty's timezone, inferred from the phone number (stated as approximate), so "tomorrow morning" means what the caller means.
- **The task brief** — on outbound hosted calls, your `reason`.

## What the agent can do

The agent's capabilities are fixed and identical on every hosted call — there is no per-customer tool configuration, and nothing it can do takes a recipient: everything is bound server-side to the person on the call. Sends go through the same pipeline as any other Inkbox send: SMS opt-outs, contact rules, and delivery tracking all apply exactly as if your own code had sent the message.

On a call, the agent can:

- **Recall your history with the caller** — past calls (including reading a previous call's transcript), SMS, iMessage, and email threads, plus keyword search across all of them. Every lookup covers only the person on the call.
- **Text, iMessage, or email the caller mid-call** — an SMS goes out from the identity's own number, an iMessage over its iMessage line, and an email from its own address (known contacts with an email on file only). It reports delivery honestly: confirmed, still on its way, or failed and why.
- **Record work for your agent** — register, refine, or withdraw post-call action items during the call; the open items become the [post-call package](#the-post-call-package).
- **Answer "what is Inkbox?"** — it can consult the public Inkbox docs and offer to text a relevant link rather than reading URLs aloud.
- **Check live state and end the call** — re-check the caller's connection state mid-call (e.g. to confirm "your message just came through"), and hang up once the caller signals they're done.

### What it can't do

Scoping is by construction, not by policy. No capability takes a recipient — the person on the call is bound server-side when the session starts, so the agent is *physically incapable* of texting, emailing, or reading history about anyone else, even if a caller tries to talk it into it. It cannot browse your contacts, touch other conversations, or reach any other Inkbox data.

It also doesn't run custom tools. If your agent needs to hit your systems mid-call, that's the bring-your-own tier: take the call over `client_websocket_url` and drive it yourself. For everything that can wait until the call ends, post-call action items cover it — the hosted agent records the work, and your agent executes it from the `call.ended` delivery.

## The post-call package

Every hosted call ends in a single [`call.ended`](/docs/api/phone/webhooks#call-ended-webhook) delivery carrying the whole story: the call record (now with `mode` and `reason`), the transcript, an `outcome`, and the recorded `post_call_action_items` — atomically, in one event. A hosted call reports back on every terminal state, inbound and placed alike — even when nobody picked up, `outcome` says what happened — so an errand can't fail silently.

| `outcome` | Meaning |
| :--- | :--- |
| `completed` | The call connected and ran to a normal end. A call that reaches voicemail also reports `completed` — the transcript shows what happened |
| `no_answer` | The call was never answered — an outbound call rang out, or an inbound caller hung up before the agent picked up |
| `declined` | The call was rejected, or the line was busy |
| `failed` | The hosted session hit an error and the call was torn down, or the call could not be completed at all — e.g. an undeliverable destination |

`outcome` is `null` on `client_websocket` calls — it describes hosted sessions only, and it rides the `call.ended` payload as `data.outcome`.

Action items arrive as an ordered list on the same event, as `data.post_call_action_items`:

```json
[
    {
      "id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
      "seq": 1,
      "action": "Confirm Tuesday 9:30am cleaning with Dr. Chen's office",
      "details": "They pencilled it in; front desk asked for a confirmation call Monday.",
      "status": "open"
    }
]
```

Each item carries a `seq` (its 1-based order), an `action` title, optional `details`, and a `status` of `"open"`. Actions the agent canceled mid-call are dropped — only open items ride the payload. The `call.ended` event is the one atomic delivery of the whole package (`outcome` lives only there); like every webhook it carries a stable ID and is replayable, so a missed delivery can be re-fetched rather than lost. The open action items are additionally readable straight off the [call object](/docs/api/phone/calls#call-object) as `post_call_action_items`, so they can be looked up any time without replaying the event.

## Watching and stopping a live call

You keep two levers while a hosted call is in progress:

- **Live view** — transcript segments land while the call runs. Poll [`GET /phone/calls/{call_id}/transcripts`](/docs/api/phone/transcripts), or watch the call live in the [Inkbox Console](https://inkbox.ai/console).
- **Kill switch** — [`POST /phone/calls/{call_id}/hangup`](/docs/api/phone/calls#hang-up-call) ends any live call from the outside, hosted or not. Teardown runs the normal lifecycle, so you still receive the full post-call package.

## Related

- [Place call](/docs/api/phone/calls#place-call) — the `mode` and `reason` request fields
- [Incoming calls](/docs/api/phone/incoming-call-action) — the `hosted_agent` action
- [Call-ended webhook](/docs/api/phone/webhooks#call-ended-webhook) — the full payload, including `outcome` and `post_call_action_items`
- [Webhooks guide](/docs/webhooks#subscribing-to-call-lifecycle-events) — subscribing and handler examples
