# Tel

URL: https://docs.hanzo.ai/docs/openapi/tel

> The telecommunications surface: phone numbers, calls and messages, on whatever carrier the deployment is configured for.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/tel) · [MCP](https://docs.hanzo.ai/docs/mcp-tools/tel) · [SDKs](https://docs.hanzo.ai/docs/sdks)

The telecommunications surface: phone numbers, calls and messages, on whatever carrier the deployment is configured for.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 10                                     |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1069 · Tel — Numbers, Calls and Messages** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1069-tel-numbers-calls-and-messages.md)

`/v1/tel` is the telecommunications surface: an org searches for and holds phone
numbers, places and ends calls, sends messages, and reads the records of all
three. A call may be handed to an assistant, which is ours.

This HIP specifies the carrier contract that keeps the surface white-label, the
honesty rule about what "sent" means, and the tenancy boundary. The implementation
is `hanzoai/cloud` `apps/tel`.

### Motivation

A telecom surface acquires a vendor faster than almost anything else: a base URL
here, a status string there, and within a release the product cannot terminate on
a different network without a rewrite. Numbering is national and regulated, so a
brand operating in another jurisdiction may have no choice about which network it
terminates on.

The second pressure is the assistant. A carrier that also answers calls with its
own voice agent is deciding what our product says. That is not a dependency worth
having.

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
in RFC 2119.

#### The carrier is an interface, and nothing above it names a network

Three verbs — numbers, calls, messages — are the whole contract
(`apps/tel/carrier.go:16`). Everything else in the package is tenancy, records and
policy on top of them.

No code above that interface may name a carrier or a brand. The concrete carrier
is built from configuration at mount, so terminating on a different network in a
different jurisdiction is an environment change rather than an edit. Credentials
come from the environment, populated from the key store on a cluster; \*_there is
no literal credential in the source and there must never be one._\*

A deployment with no carrier credential falls back to a carrier that answers
without a network, whose numbers are in the range reserved for fiction. That is
also what the tests run against, so the surface is exercised end to end without
placing a call.

#### Acceptance is not delivery

The carrier answers with an identifier as soon as it has **accepted** a request.
The outcome — connected, delivered, failed — arrives later on the event stream.

A message submission MUST NOT be recorded as delivered because the submission
succeeded. A carrier that returns "sent" synchronously is telling you it accepted
the request, and recording that as delivery is how a message that never arrived
becomes a message the record says arrived.

The door that would receive those events does not exist yet: this package mounts
no carrier callback route and consumes no stream, so today a record's status is
what the carrier answered synchronously plus the transitions this surface itself
makes — a hangup, a release (apps/tel/store.go:163). The rule above is the
contract the inbound half lands against, not a description of code that runs.

#### Assistants are ours

A call handed to an agent is answered by a Hanzo assistant on Hanzo inference: the
same models, prompts and tools every other surface uses. The carrier moves the
audio and does not decide what is said.

The package reaches the platform's own AI door rather than a model package
(`apps/tel/agent.go:24`), so an assistant improved for chat is improved for calls
in the same release, and which model answers is the catalog's decision behind that
door rather than a constant in a telecom package.

#### Tenancy

The org is the value minted from the validated bearer's owner claim (HIP-0026),
enforced server-side on every request and never a client-supplied header. Every
store query filters on it, so one tenant can neither read nor mutate another's
numbers, calls or messages.

#### The surface is an exact set

Every route is a typed operation, and the served set is pinned as an **exact set**
rather than a floor, so a route added here fails whether or not anyone remembers
the test (`apps/tel/typed_wire_test.go`).

There is no untyped-by-design ledger, because nothing on this surface refuses: no
raw bytes, no multi-status answer, no verbatim relay. The day one appears it is
named with its wire fact and the set still has to match.

The addresses are the operations at `/v1/tel` (plugin/tel/openapi.json): number
search (`/numbers/available`), the org's numbers (list, buy, release), calls
(list, place, hang up), messages (list, send), and the per-org `/summary`
roll-up.

#### What it owns, charges and emits

The store is one SQL database opened at mount (`sqlpool.Open("tel", …)`,
apps/tel/store.go:21) with three tables — numbers, calls, messages — each
leading its primary key with the org, so isolation is a physical property of the
row rather than a WHERE clause somebody has to remember.

It is metered: the plugin declares `Price: cloud.Metered`
(plugin/tel/main.go:24) and the meter is `apps/tel/meter.go`. Three acts are
billed — a number order at the platform's flat provision fee (100 cents,
`cloud.DefaultResourceFeeCents`), a message at 1 cent, a call placement at
1 cent — each movable by the `TEL_FEE_CENTS` knobs, with 0 making an act free
and un-gated again. A call is priced per placement, not per minute: the
completion callback goes to the customer's own webhook, so the duration never
reaches this process and a minute is not a unit this binary observes. The
debit lands through the metering plane (`cloud.Charge.Debit` with a
`metering.Usage` row): authorized before the carrier is asked, debited only
after the carrier confirms, so failed work is not billed. A deployment on the
stub holds no carrier credential, buys nothing, and is neither gated nor
billed.

It publishes no events on the platform bus, so a customer's webhooks (HIP-1310)
receive nothing from it, and it emits nothing to observability beyond the
request span every route gets.

The stage is `ga` — the manifest row declares none, and absent is `ga`
(HIP-0139 §8). The capability derives from no OSS upstream: the carrier is
reached over its HTTP API behind the interface (`apps/tel/rest.go`), configured
by `TEL_CARRIER_BASE` and `TEL_CARRIER_KEY`, and the fallback carrier is this
package's own stub.

### Rationale

The alternative is to build against one carrier's API and generalize later.
Measured against how this surface actually changes — a brand, a jurisdiction, a
regulator — later is exactly when generalizing is most expensive, because by then
the vendor's vocabulary has leaked into the records, the status values and the
console. Three verbs behind an interface cost one indirection now.

### Security Considerations

The tenant boundary is server-side on every request and derives from a validated
credential rather than an asserted header, because the objects behind it are
billable and externally visible: a number bought under another tenant's org is a
charge and a phone line.

Carrier credentials live in configuration sourced from the key store and never
appear in a response. Records carry what the carrier reported: with no event
door yet, a status stays at what the carrier acknowledged, so the record cannot
claim an outcome nobody observed.

## Four surfaces

| Surface  | Reaches this capability as                  | Coverage                                                                   |
| -------- | ------------------------------------------- | -------------------------------------------------------------------------- |
| **REST** | `tel` at its own prefix                     | 10 operations                                                              |
| **CLI**  | `hanzo tel …`                               | 10 of 10                                                                   |
| **SDK**  | `TelApi` in every published client          | 10 methods                                                                 |
| **MCP**  | tool `tel` on `https://api.hanzo.ai/v1/mcp` | 10 operations, 1 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/tel/calls`, operation `get_tel_calls`:

**CLI**

```bash
hanzo tel calls list
```

**SDK**

**.ts**

```ts
import { Configuration, TelApi } from 'hanzoai';

const api = new TelApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getTelCalls();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import TelApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = TelApi(client).get_tel_calls()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.TelAPI.GetTelCalls(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, tel_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = tel_api::get_tel_calls(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.TelApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new TelApi(client).getTelCalls();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/tel/calls \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **tel** through the `tel` tool, which names its 10 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_tel_calls"
         }
       }
     }'
```

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                            | What it does                                                                                |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [`DELETE /v1/tel/calls/{id}`](https://docs.hanzo.ai/docs/openapi/tel/delete-tel-calls-by-id)        | Ends a call this org placed.                                                                |
| [`GET /v1/tel/calls`](https://docs.hanzo.ai/docs/openapi/tel/get-tel-calls)                         | Lists the calls this org has placed or received, newest first.                              |
| [`POST /v1/tel/calls`](https://docs.hanzo.ai/docs/openapi/tel/post-tel-calls)                       | Dials.                                                                                      |
| [`GET /v1/tel/messages`](https://docs.hanzo.ai/docs/openapi/tel/get-tel-messages)                   | Lists the messages this org has sent or received, newest first.                             |
| [`POST /v1/tel/messages`](https://docs.hanzo.ai/docs/openapi/tel/post-tel-messages)                 | Sends a message from one of this org's own numbers.                                         |
| [`DELETE /v1/tel/numbers/{id}`](https://docs.hanzo.ai/docs/openapi/tel/delete-tel-numbers-by-id)    | Checks the holding is THIS org's before it reaches the carrier.                             |
| [`GET /v1/tel/numbers/available`](https://docs.hanzo.ai/docs/openapi/tel/get-tel-numbers-available) | Asks the carrier what is available to buy.                                                  |
| [`GET /v1/tel/numbers`](https://docs.hanzo.ai/docs/openapi/tel/get-tel-numbers)                     | Lists the phone numbers this org HOLDS — the ones it has bought and not released.           |
| [`POST /v1/tel/numbers`](https://docs.hanzo.ai/docs/openapi/tel/post-tel-numbers)                   | Provisions with the carrier FIRST and records second.                                       |
| [`GET /v1/tel/summary`](https://docs.hanzo.ai/docs/openapi/tel/get-tel-summary)                     | Counts what this org holds on the telephony plane: its numbers, its calls and its messages. |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
