# Numbers

> List your phone numbers, search carrier inventory, buy local or toll-free numbers, and route a number to or away from the AI agent.

Source: https://dev.ody.co/api/numbers

A **number** is a real phone line owned by the workspace. Numbers you buy through the API are indistinguishable from numbers bought in the app: they show up in **Settings → Numbers**, they're billed on the same subscription, and a teammate can re-route them at any time.

## The number object

- `e164` *string* — The number, for example `+15125550100`.
- `type` *"local" | "toll_free"*
- `voiceMode` *string* — Who answers inbound calls: `agent` (Astra), `app` (rings your team), `voicemail` or `forward`.
- `agentId` *uuid | null* — The AI agent, when `voiceMode` is `agent`.
- `forwardTo` *string | null* — The forwarding target, when `voiceMode` is `forward`.
- `assignedUserId` *string | null* — The teammate the line belongs to, if it's a personal line.
- `status` *string* — `active` once the line is live.

## Pricing

| | Monthly | Notes |
|---|---|---|
| First local number | Included in the plan | |
| Each extra local number | **$5/mo** add-on | Prorated on the subscription |
| Toll-free (+1 8xx) | **$10/mo** add-on | Calling only. Texting from toll-free isn't available yet |
| Texting on a local number | One-time **$20** setup per number + $4/mo messaging add-on | Charged when you file [10DLC](https://dev.ody.co/api/messaging) |

Ody works out the billed type from the number itself, so a +1 8xx number is always billed as toll-free whatever `type` you send.

**`GET /v1/api/numbers`**

### List numbers

Returns every number in the workspace.

**Scope:** `numbers:read`

**Returns** `{ numbers: Number[] }`.

cURL:

```bash
curl https://api.ody.co/v1/api/numbers \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const numbers = await ody.numbers.list();
```
Python:

```python
numbers = client.numbers.list()
```

Response:

```json
{
  "numbers": [
    {
      "e164": "+15125550100",
      "type": "local",
      "voiceMode": "agent",
      "agentId": "0d9c…",
      "forwardTo": null,
      "assignedUserId": null,
      "status": "active"
    }
  ]
}
```

**`GET /v1/api/numbers/available`**

### Search available numbers

Searches live carrier inventory. Each result is quoted at Ody's retail monthly price: $5 for local (the first local number is included in the plan), $10 for toll-free.

**Scope:** `numbers:manage`

**Query parameters**

- `country` *string* — ISO 3166 alpha-2. Default `US`.
- `type` *"local" | "toll_free"* — Default `local`.
- `startsWith` *string* — Leading digits, usually the area code (for example `512`). Local numbers only.
- `contains` *string* — Digits anywhere in the number.
- `locality` *string* — City.
- `state` *string* — State or region (for example `TX`).
- `limit` *integer* — 1–50, default 20.

**Returns** `{ available: [{ phone_number, phone_number_type, region_information, cost_information }] }`. These fields are snake_case because they come straight from the carrier.

cURL:

```bash
curl "https://api.ody.co/v1/api/numbers/available?startsWith=512&limit=5" \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
const available = await ody.numbers.searchAvailable({ startsWith: "512", limit: 5 });
```
Python:

```python
available = client.numbers.search_available(starts_with="512", limit=5)
```

Response:

```json
{
  "available": [
    {
      "phone_number": "+15125550188",
      "phone_number_type": "local",
      "region_information": [{ "region_type": "rate_center", "region_name": "AUSTIN" }],
      "cost_information": { "monthly_cost": "5.00", "upfront_cost": "0.00", "currency": "USD" }
    }
  ]
}
```

**`POST /v1/api/numbers`**

### Buy a number

Orders the number and bills the workspace exactly like a purchase in the app. The number is live as soon as the call returns. Inbound calls ring the team (`voiceMode: "app"`) until you [route it to the agent](#route-a-number-to-the-agent).

The same **hard gates** as the app apply. The workspace must have chosen a plan and completed identity and mobile verification where required. Workspaces billed through the Apple App Store can't add paid add-ons (a second number or a toll-free line). An agent can never buy past these gates.

With a **test key** nothing is ordered or charged; the response has `simulated: true`.

**Scope:** `numbers:manage` · supports [`Idempotency-Key`](https://dev.ody.co/api/idempotency)

**Body**

- `phoneNumber` *string* **required** — An E.164 number from [Search available numbers](#search-available-numbers).
- `type` *"local" | "toll_free"* — A hint only. Ody works out the real type from the number.

**Returns** `201` with `{ number, simulated? }`.

**Errors (`409`):** "Choose a plan before adding phone numbers", "Verify your identity before adding phone numbers", "Verify your mobile number before adding phone numbers", the App Store add-on block, or the carrier's message when the number was taken between search and order.

cURL:

```bash
curl https://api.ody.co/v1/api/numbers \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: onboard-acme-main-line" \
  -d '{"phoneNumber":"+15125550188"}'
```
TypeScript:

```ts
const { number } = await ody.numbers.buy({
  phoneNumber: "+15125550188",
  idempotencyKey: "onboard-acme-main-line",
});
```
Python:

```python
res = client.numbers.buy(phone_number="+15125550188", idempotency_key="onboard-acme-main-line")
```

**`POST /v1/api/numbers/:e164/connect`**

### Route a number to the agent

Sends the number's inbound calls to the AI agent. The agent must already exist: create it with [`PATCH /agent`](https://dev.ody.co/api/agent#update-the-agent) first. Encode `+` as `%2B` in the path.

**Scope:** `agent:manage`

**Returns** `{ ok: true, voiceMode: "agent" }`. Returns `404` if the number isn't in the workspace, or `409` "Create an agent first".

cURL:

```bash
curl -X POST https://api.ody.co/v1/api/numbers/%2B15125550188/connect \
  -H "Authorization: Bearer $ODY_API_KEY"
```
TypeScript:

```ts
await ody.numbers.connectAgent("+15125550188");
```
Python:

```python
client.numbers.connect_agent("+15125550188")
```

**`POST /v1/api/numbers/:e164/disconnect`**

### Route a number away from the agent

Takes the agent off the line and chooses who answers instead.

**Scope:** `agent:manage`

**Body** (optional)

- `mode` *"app" | "voicemail" | "forward"* — Default `app` (ring the team).
- `forwardTo` *string* — E.164. Required when `mode` is `forward`.

**Returns** `{ ok: true, voiceMode }`.

cURL:

```bash
curl -X POST https://api.ody.co/v1/api/numbers/%2B15125550188/disconnect \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode":"forward","forwardTo":"+15125559999"}'
```
TypeScript:

```ts
await ody.numbers.disconnectAgent("+15125550188", { mode: "forward", forwardTo: "+15125559999" });
```
Python:

```python
client.numbers.disconnect_agent("+15125550188", mode="forward", forward_to="+15125559999")
```
