> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Phone numbers

A phone number is the voice line your agents answer and call from. Each number belongs to exactly one workspace and is bound to **one agent**: calls to the number reach that agent, and calls the agent places show that number as caller ID. You can get a number three ways: buy one through Jelliu, connect a number you already own over your SIP trunk, or keep your existing line and forward its calls to a Jelliu number.

The Phone Numbers API lets you browse available inventory, provision numbers, connect your own, change which agent answers, configure a phone menu, and get the dialing codes to forward a line.

## How it works

```mermaid
flowchart LR
    A["GET /available<br />browse inventory"] --> B["POST /provision<br />buy a number"]
    C["POST /connect-sip<br />your own number"] --> N
    B --> N[("Phone number<br />bound to one agent")]
    D["Plan add-on<br />bought in the dashboard"] --> U["Number without an agent"]
    U -- "PATCH agentId" --> N
    N -- "inbound call" --> G{"Workspace checks<br />suspension, trial minutes,<br />concurrent calls"}
    G -- "phone menu on" --> M["Caller presses a digit"] --> R["Agent answers"]
    G -- "phone menu off" --> R
    N -- "caller ID for outbound calls" --> O["Calls placed by the agent"]
```

**Getting a number**

| Path                         | Endpoint                                              | What happens                                                                                                                                                       |
| ---------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Buy the plan-included number | `POST /api/phone-numbers/provision`                   | Jelliu buys a voice-capable number in the US or Canada, registers it for voice and binds your agent, in one request.                                               |
| Buy additional numbers       | Dashboard, **Settings → Plan**                        | A paid add-on. After payment the numbers are bought in the country you chose and appear in `GET /api/phone-numbers` **without an agent**; assign one with `PATCH`. |
| Connect a number you own     | `POST /api/phone-numbers/connect-sip`                 | Nothing is bought. Your number is connected over your SIP trunk, bound to the agent, and then answers and dials exactly like a purchased number.                   |
| Forward an existing line     | `GET /api/phone-numbers/{id}/forwarding-instructions` | Nothing changes on Jelliu's side. Your carrier forwards the line to a Jelliu number, where its agent answers.                                                      |

**Inbound calls.** The number you call is the routing key: it identifies the workspace and the agent. Before an agent answers, Jelliu checks that the workspace is not suspended, that a trialing workspace still has trial minutes, and that a concurrent-call slot is free. If the workspace is suspended or out of trial minutes, the caller hears a short message and the call ends; if every slot is busy, the caller is told that all agents are busy.

* If the number has an agent, **that agent answers**. If that agent is paused, the call is **not** handed to a different agent.
* If the phone menu is enabled, the caller hears the greeting and the options, and has 10 seconds to press one digit. The option's agent answers; if it is unavailable, Jelliu tries the option's department, then another agent in the workspace.

**Outbound calls.** When an agent places a call, the caller ID is the workspace's active number bound to that agent, or otherwise its most recently updated active number. See [Calls](/resources/calls).

A workspace on an **active paid plan** with no active, registered number cannot place calls: they fail with `403 PHONE_NUMBER_REQUIRED`. Provision or connect a number before starting a voice campaign.

## The phone number object

Returned by every endpoint on this page except `GET /available` and `GET /{id}/forwarding-instructions`.

| Field              | Type               | Nullable | Description                                                                                                                                      |
| ------------------ | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`               | string (uuid)      | No       | Identifier of the number in Jelliu.                                                                                                              |
| `phoneNumber`      | string             | No       | The number in E.164 format, for example `+13055550142`. A number can exist in only one Jelliu workspace.                                         |
| `label`            | string             | Yes      | Display name. Defaults to `Jelliu +13055550142` for provisioned numbers and `+573001234567 (own number)` for connected ones.                     |
| `isActive`         | boolean            | No       | Inactive numbers are never used as caller ID and do not count toward your plan's number limit.                                                   |
| `agentId`          | string (uuid)      | Yes      | The agent that answers the number. `null` when none is assigned.                                                                                 |
| `agentName`        | string             | Yes      | Name of that agent.                                                                                                                              |
| `agentProvisioned` | boolean            | Yes      | `false` when the assigned agent is still being set up, so calls will not reach it yet. `null` when no agent is assigned.                         |
| `ivrEnabled`       | boolean            | No       | Whether callers hear the phone menu before an agent answers.                                                                                     |
| `ivrGreeting`      | string             | Yes      | Greeting read before the menu options.                                                                                                           |
| `ivrOptions`       | object\[]          | No       | Menu options, each `{ digit, label, agentId, department }`. Empty array when there is no menu.                                                   |
| `channels`         | string\[]          | No       | What the number answers on: `["voice"]`, `["whatsapp"]`, or both. A WhatsApp line you connected also appears in this list.                       |
| `whatsappStatus`   | string             | Yes      | Status of the WhatsApp sender on this number (`CREATING`, `VERIFYING`, `ONLINE`, `OFFLINE`, `FAILED`), or `null` when it is not a WhatsApp line. |
| `provider`         | string             | No       | `twilio` for a number bought through Jelliu, `sip_trunk` for your own number connected over SIP.                                                 |
| `sipHost`          | string             | Yes      | For `sip_trunk` numbers, the outbound trunk address you supplied. `null` otherwise.                                                              |
| `createdAt`        | string (date-time) | No       | When the number was added.                                                                                                                       |
| `updatedAt`        | string (date-time) | No       | Last change.                                                                                                                                     |

```json
{
  "data": {
    "id": "3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77",
    "phoneNumber": "+13055550142",
    "label": "Miami front desk",
    "isActive": true,
    "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "agentName": "Recepción",
    "agentProvisioned": true,
    "ivrEnabled": false,
    "ivrGreeting": null,
    "ivrOptions": [],
    "channels": ["voice"],
    "whatsappStatus": null,
    "provider": "twilio",
    "sipHost": null,
    "createdAt": "2026-09-01T14:03:22.114Z",
    "updatedAt": "2026-09-10T09:41:05.870Z"
  }
}
```

## Endpoints

| Method  | Path                                              | Required    |
| ------- | ------------------------------------------------- | ----------- |
| `GET`   | `/api/phone-numbers`                              | `read` key  |
| `GET`   | `/api/phone-numbers/{id}`                         | `read` key  |
| `GET`   | `/api/phone-numbers/available`                    | `read` key  |
| `GET`   | `/api/phone-numbers/{id}/forwarding-instructions` | `read` key  |
| `PATCH` | `/api/phone-numbers/{id}`                         | `write` key |
| `POST`  | `/api/phone-numbers/provision`                    | `full` key  |
| `POST`  | `/api/phone-numbers/connect-sip`                  | `full` key  |

`GET /api/phone-numbers` returns every number in the workspace, oldest first, in `{ "data": [ ... ] }`. It takes no paging parameters.

The API has no endpoint to release or delete a number.

## Common tasks

### Buy a number

#### Browse available numbers

Search the inventory for a country. Nothing is bought.

**`curl`**

```bash title="curl"
curl -sS "https://api.jelliu.co/api/phone-numbers/available?country=US&type=local&areaCode=305&limit=5" \
  -H "Authorization: Bearer $JELLIU_API_KEY"
```

**`Node.js`**

```javascript title="Node.js"
const url = new URL('https://api.jelliu.co/api/phone-numbers/available');
url.search = new URLSearchParams({ country: 'US', type: 'local', areaCode: '305', limit: '5' });

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.JELLIU_API_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

for (const n of body.data) console.log(n.phoneNumber, n.locality, n.addressRequired);
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.get(
    "https://api.jelliu.co/api/phone-numbers/available",
    params={"country": "US", "type": "local", "areaCode": "305", "limit": 5},
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=30,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

for n in body["data"]:
    print(n["phoneNumber"], n["locality"], n["addressRequired"])
```

```json
{
  "data": [
    {
      "phoneNumber": "+13055550142",
      "friendlyName": "(305) 555-0142",
      "locality": "Miami",
      "region": "FL",
      "isoCountry": "US",
      "type": "local",
      "capabilities": { "voice": true, "sms": true },
      "addressRequired": false
    }
  ]
}
```

**`country`** `string` — required

ISO 3166-1 alpha-2 code, case-insensitive. Must be a country where Jelliu can provision numbers (see [Countries](#countries)).

---

**`type`** `string`

`local`, `mobile` or `tollfree`. When omitted, all three types are searched and combined.

---

**`areaCode`** `string`

Three-digit area code. Applied only in the US and Canada; ignored elsewhere.

---

**`contains`** `string`

Pattern the number must contain: 1 to 15 digits, letters or `*`.

---

**`limit`** `integer` — default: 10

1 to 30. Applies **per type**, so a search without `type` can return up to three times this many numbers.

---

Only voice-capable numbers are returned. A country with no stock of the requested type returns an empty list. `addressRequired: true` means the number is subject to a local regulatory address requirement.

#### Provision it for an agent

Buy the number and bind the agent that answers it. The agent must belong to your workspace and have finished provisioning.

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/phone-numbers/provision" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "country": "US",
    "phoneNumber": "+13055550142",
    "label": "Miami front desk"
  }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/phone-numbers/provision', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentId: '7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4',
    country: 'US',
    phoneNumber: '+13055550142',
    label: 'Miami front desk',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

console.log(body.data.id, body.data.phoneNumber); // 201 Created
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/api/phone-numbers/provision",
    json={
        "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
        "country": "US",
        "phoneNumber": "+13055550142",
        "label": "Miami front desk",
    },
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=60,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["data"]["id"], body["data"]["phoneNumber"])
```

The response is `201 Created` with the [phone number object](#the-phone-number-object) in `data`.

**`agentId`** `string (uuid)` — required

The agent that answers the number.

---

**`country`** `string` — default: US

`US` or `CA`. Any other serviceable country returns `402 BILLING_ERROR`: numbers elsewhere are paid add-ons bought under **Settings → Plan**.

---

**`phoneNumber`** `string`

Exact E.164 number to buy, taken from `GET /available`. Without it, Jelliu picks the first available number, trying `local` and then `mobile`.

---

**`areaCode`** `string`

Three-digit area code to prefer when Jelliu picks the number.

---

**`type`** `string`

`local`, `mobile` or `tollfree` when Jelliu picks the number.

---

**`label`** `string`

1 to 120 characters.

---

Provisioning is safe to retry. If the agent already has an active number, the request buys nothing and returns that number with `201`, even if you pass a different `country` or `phoneNumber`. A second request for the same agent while the first is still running returns `409 CALL_ALREADY_IN_PROGRESS`.

If registering the number or binding the agent fails after the number was bought, Jelliu releases the number and the request fails, so a failed request does not leave a half-configured number in your workspace.

### Assign or change the agent

Send only the fields you want to change. `null` clears a nullable field; an omitted field is left untouched.

**`curl`**

```bash title="curl"
curl -sS -X PATCH "https://api.jelliu.co/api/phone-numbers/3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "b2e4d6f8-1a3c-4e5f-8a9b-0c1d2e3f4a5b", "label": "Soporte Bogotá" }'
```

**`Node.js`**

```javascript title="Node.js"
const numberId = '3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77';
const res = await fetch(`https://api.jelliu.co/api/phone-numbers/${numberId}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentId: 'b2e4d6f8-1a3c-4e5f-8a9b-0c1d2e3f4a5b',
    label: 'Soporte Bogotá',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

console.log(body.data.agentName, body.data.agentProvisioned);
```

**`Python`**

```python title="Python"
import os
import requests

number_id = "3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77"
res = requests.patch(
    f"https://api.jelliu.co/api/phone-numbers/{number_id}",
    json={"agentId": "b2e4d6f8-1a3c-4e5f-8a9b-0c1d2e3f4a5b", "label": "Soporte Bogotá"},
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=60,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["data"]["agentName"], body["data"]["agentProvisioned"])
```

The response is `200` with the updated number in `data`. Changes apply to the next call.

**`agentId`** `string (uuid) | null`

The agent that answers. It must serve every channel in the number's `channels`: a WhatsApp line needs an agent that answers WhatsApp. `null` unassigns the agent.

---

**`label`** `string | null`

Up to 120 characters.

---

**`isActive`** `boolean`

Deactivate or reactivate the number.

---

**`ivrEnabled`** `boolean`

Turn the phone menu on or off. See [Configure a phone menu](#configure-a-phone-menu).

---

**`ivrGreeting`** `string | null`

Up to 1000 characters.

---

**`ivrOptions`** `object[]`

Up to 20 options with unique digits. `[]` clears the menu.

---

If a number is currently answered by an agent that was configured outside Jelliu, assigning a different agent returns `409 VALIDATION_FAILED` so the line is not taken over by accident. Send `{ "agentId": null }` first, then assign the new agent.

Numbers bought as plan add-ons arrive with `agentId: null`. Assigning an agent is what registers them for voice, so do it before you rely on the number.

### Configure a phone menu

A phone menu lets callers choose where their call goes. Each option routes to a specific agent or to a department; when a department is given, the oldest available agent in that department answers.

**`curl`**

```bash title="curl"
curl -sS -X PATCH "https://api.jelliu.co/api/phone-numbers/3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ivrEnabled": true,
    "ivrGreeting": "Gracias por llamar a Clínica Aurora.",
    "ivrOptions": [
      { "digit": "1", "label": "citas", "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4" },
      { "digit": "2", "label": "facturación", "department": "billing" }
    ]
  }'
```

**`Node.js`**

```javascript title="Node.js"
const numberId = '3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77';
const res = await fetch(`https://api.jelliu.co/api/phone-numbers/${numberId}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    ivrEnabled: true,
    ivrGreeting: 'Gracias por llamar a Clínica Aurora.',
    ivrOptions: [
      { digit: '1', label: 'citas', agentId: '7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4' },
      { digit: '2', label: 'facturación', department: 'billing' },
    ],
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

console.log(body.data.ivrEnabled, body.data.ivrOptions.length);
```

**`Python`**

```python title="Python"
import os
import requests

number_id = "3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77"
res = requests.patch(
    f"https://api.jelliu.co/api/phone-numbers/{number_id}",
    json={
        "ivrEnabled": True,
        "ivrGreeting": "Gracias por llamar a Clínica Aurora.",
        "ivrOptions": [
            {"digit": "1", "label": "citas", "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4"},
            {"digit": "2", "label": "facturación", "department": "billing"},
        ],
    },
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=60,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["data"]["ivrEnabled"], len(body["data"]["ivrOptions"]))
```

Each option in `ivrOptions`:

| Field        | Type                  | Required | Description                                                    |
| ------------ | --------------------- | -------- | -------------------------------------------------------------- |
| `digit`      | string                | Yes      | One of `0`-`9`, `*` or `#`. Unique within the menu.            |
| `label`      | string                | Yes      | 1 to 120 characters, read to the caller.                       |
| `agentId`    | string (uuid) or null | No       | Agent that answers this option. Must belong to your workspace. |
| `department` | string or null        | No       | 1 to 80 characters. Routes to an agent in this department.     |

The phone menu exists only on numbers bought through Jelliu (`provider: "twilio"`). Enabling it on a number connected over your SIP trunk, or on a WhatsApp-only line, returns `400 VALIDATION_FAILED`. A menu also needs at least one option: `ivrEnabled: true` with no options is refused.

### Connect a number you already own

Bring your number over your SIP trunk. Give `inbound` so the agent answers calls on the number, `outbound` so it can call from it, or both.

**`curl`**

```bash title="curl"
curl -sS -X POST "https://api.jelliu.co/api/phone-numbers/connect-sip" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "phoneNumber": "+576015550199",
    "label": "Línea principal",
    "inbound": {
      "allowedAddresses": ["203.0.113.10/32"],
      "mediaEncryption": "allowed"
    },
    "outbound": {
      "address": "sip.carrier.example.com:5061",
      "transport": "tls",
      "credentials": { "username": "jelliu-trunk", "password": "s3cr3t-trunk-pass" },
      "enabledCodecs": ["PCMU/8000", "PCMA/8000"]
    }
  }'
```

**`Node.js`**

```javascript title="Node.js"
const res = await fetch('https://api.jelliu.co/api/phone-numbers/connect-sip', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    agentId: '7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4',
    phoneNumber: '+576015550199',
    label: 'Línea principal',
    inbound: {
      allowedAddresses: ['203.0.113.10/32'],
      mediaEncryption: 'allowed',
    },
    outbound: {
      address: 'sip.carrier.example.com:5061',
      transport: 'tls',
      credentials: { username: 'jelliu-trunk', password: process.env.SIP_TRUNK_PASSWORD },
      enabledCodecs: ['PCMU/8000', 'PCMA/8000'],
    },
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

console.log(body.data.provider, body.data.sipHost); // "sip_trunk", "sip.carrier.example.com:5061"
```

**`Python`**

```python title="Python"
import os
import requests

res = requests.post(
    "https://api.jelliu.co/api/phone-numbers/connect-sip",
    json={
        "agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
        "phoneNumber": "+576015550199",
        "label": "Línea principal",
        "inbound": {
            "allowedAddresses": ["203.0.113.10/32"],
            "mediaEncryption": "allowed",
        },
        "outbound": {
            "address": "sip.carrier.example.com:5061",
            "transport": "tls",
            "credentials": {"username": "jelliu-trunk", "password": os.environ["SIP_TRUNK_PASSWORD"]},
            "enabledCodecs": ["PCMU/8000", "PCMA/8000"],
        },
    },
    headers={"Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}"},
    timeout=60,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["data"]["provider"], body["data"]["sipHost"])
```

The response is `201 Created` with the number in `data`, `provider: "sip_trunk"` and `sipHost` set to the outbound address.

**Top-level fields**

| Field         | Type          | Required       | Description                                                     |
| ------------- | ------------- | -------------- | --------------------------------------------------------------- |
| `agentId`     | string (uuid) | Yes            | A provisioned agent in your workspace that answers voice calls. |
| `phoneNumber` | string        | Yes            | Your number in E.164 format.                                    |
| `label`       | string        | No             | 1 to 120 characters.                                            |
| `inbound`     | object        | One of the two | What your PBX or carrier sends to Jelliu.                       |
| `outbound`    | object        | One of the two | Where Jelliu sends calls placed from this number.               |

**`inbound`**: must include `allowedAddresses` or `credentials`, so the trunk does not accept calls from anyone.

| Field              | Type      | Description                                                                                          |
| ------------------ | --------- | ---------------------------------------------------------------------------------------------------- |
| `allowedAddresses` | string\[] | Up to 50 IP addresses or CIDR blocks allowed to send calls.                                          |
| `allowedNumbers`   | string\[] | Up to 200 E.164 numbers.                                                                             |
| `credentials`      | object    | `{ username, password }` for digest authentication. Username 1 to 120 characters, password 1 to 256. |
| `mediaEncryption`  | string    | `disabled`, `allowed` or `required`.                                                                 |
| `remoteDomains`    | string\[] | Up to 20 domains, each 1 to 253 characters.                                                          |

**`outbound`**

| Field             | Type      | Description                                                                                       |
| ----------------- | --------- | ------------------------------------------------------------------------------------------------- |
| `address`         | string    | **Required.** Hostname or IP address, optionally with `:port`. Not a URL: `https://` is rejected. |
| `transport`       | string    | `auto`, `udp`, `tcp` or `tls`.                                                                    |
| `mediaEncryption` | string    | `disabled`, `allowed` or `required`.                                                              |
| `credentials`     | object    | `{ username, password }`, same bounds as inbound.                                                 |
| `headers`         | object    | Custom SIP headers: names up to 64 characters, values up to 512.                                  |
| `enabledCodecs`   | string\[] | Up to 3 of `G722/8000`, `PCMU/8000`, `PCMA/8000`.                                                 |

Trunk credentials are passed to the voice platform and are **not stored by Jelliu**. The only trunk detail kept is `sipHost`, so you can see where the number lives.

If the number is already your workspace's **WhatsApp line**, connecting it over SIP adds voice to that same number instead of failing as a duplicate. Because one number is answered by one agent, the agent must serve both voice and WhatsApp; otherwise the request returns `400 VALIDATION_FAILED`.

Unknown fields in the body are ignored rather than rejected. Double-check field names such as `allowedAddresses` and `enabledCodecs`: a misspelled one is dropped silently.

### Forward an existing line

If your business line cannot connect over SIP, forward it to a Jelliu number. Fetch the dialing codes for that number; they are standard mobile call-forwarding codes, dialed from the phone the line is on.

**`curl`**

```bash title="curl"
curl -sS "https://api.jelliu.co/api/phone-numbers/3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77/forwarding-instructions" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Accept-Language: en"
```

**`Node.js`**

```javascript title="Node.js"
const numberId = '3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77';
const res = await fetch(
  `https://api.jelliu.co/api/phone-numbers/${numberId}/forwarding-instructions`,
  {
    headers: {
      Authorization: `Bearer ${process.env.JELLIU_API_KEY}`,
      'Accept-Language': 'es',
    },
  },
);
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}`);

console.log(body.data.codes.activateAll);
```

**`Python`**

```python title="Python"
import os
import requests

number_id = "3f6a2c1e-8b4d-4e7a-9c2f-5d1b0a9e8c77"
res = requests.get(
    f"https://api.jelliu.co/api/phone-numbers/{number_id}/forwarding-instructions",
    headers={
        "Authorization": f"Bearer {os.environ['JELLIU_API_KEY']}",
        "Accept-Language": "es",
    },
    timeout=30,
)
body = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {body['error']['code']}")

print(body["data"]["codes"]["activateAll"])
```

```json
{
  "data": {
    "forwardTo": "+13055550142",
    "agentName": "Recepción",
    "codes": {
      "activateAll": "**21*+13055550142#",
      "activateWhenBusy": "**67*+13055550142#",
      "activateWhenUnanswered": "**61*+13055550142#",
      "activateWhenUnreachable": "**62*+13055550142#",
      "deactivateAll": "##002#",
      "check": "*#21#"
    },
    "notes": [
      "Dial the code from the phone the line is on, then press call. The carrier confirms on screen.",
      "Every call to your number then lands on +13055550142, where Recepción answers. Your customers keep dialling the number they always have.",
      "Forwarding is a service of YOUR carrier: some block forwarding to a number abroad, and the forwarded leg is billed by them at their rate. Check with the carrier if the code is refused.",
      "This covers INCOMING calls. Calls the agent places still show the Jelliu number as caller ID; showing your own number on outbound needs a SIP trunk (Connect own number) or porting.",
      "Fixed lines and PBXs use their own forwarding menus — the codes above are for mobile lines."
    ]
  }
}
```

`notes` is written for the person dialing the code: in Spanish when `Accept-Language` starts with `es`, in English otherwise. The codes are the same in both.

The number must be a voice number, have an agent assigned and be active; otherwise the request returns `400 VALIDATION_FAILED`.

## Countries

Numbers can be searched and bought in these countries:

| Region        | Countries                    |
| ------------- | ---------------------------- |
| North America | `US`, `CA`, `PR`, `MX`, `PA` |
| South America | `AR`, `BR`, `CL`, `CO`       |
| Europe        | `DE`, `FR`, `GB`             |
| Oceania       | `AU`                         |

Any other country code returns `400 VALIDATION_FAILED` with the list of available countries in the message.

* **Plan-included number** (`POST /provision`): `US` and `CA` only.
* **Add-on numbers** (dashboard, **Settings → Plan**): any country above, priced by country.
* **Connected numbers** (`POST /connect-sip`): not limited by this list; any E.164 number your trunk carries.

Some countries sell mostly mobile numbers, which is why provisioning without `type` falls back from `local` to `mobile`.

## Errors

Every error uses the standard [error envelope](/errors). Validation failures on these endpoints return `details` as a field map (`formErrors`, `fieldErrors`).

| Code                       | Status | When                                                                                                                                                                                                                                                                                                               |
| -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `VALIDATION_FAILED`        | 400    | The body or query failed validation: bad UUID, non-E.164 number, an unsupported country, `connect-sip` without `inbound` or `outbound`, an `inbound` block without `allowedAddresses` or `credentials`, or a `PATCH` with no fields.                                                                               |
| `VALIDATION_FAILED`        | 400    | The request conflicts with the number or agent: the agent does not serve the number's channels (or does not answer voice, on `connect-sip`), a phone menu enabled without options or on a SIP or WhatsApp-only number, or forwarding instructions for a number that is inactive, has no agent or is WhatsApp-only. |
| `VALIDATION_FAILED`        | 409    | `connect-sip`: the number already exists as a voice number in your workspace, or in another Jelliu workspace. `PATCH`: the number is answered by an agent configured outside Jelliu; clear `agentId` first.                                                                                                        |
| `BILLING_ERROR`            | 402    | `provision`: a `country` other than `US` or `CA`, or the workspace already holds all the numbers it is entitled to. `connect-sip`: the plan's number limit is reached. On the limit errors, `metadata.tier` names the current plan.                                                                                |
| `AGENT_NOT_FOUND`          | 404    | The agent, or an agent in `ivrOptions`, does not exist in your workspace.                                                                                                                                                                                                                                          |
| `AGENT_PROVISIONING`       | 409    | The agent is still being set up. Retry after `Retry-After` (5 seconds).                                                                                                                                                                                                                                            |
| `CALL_ALREADY_IN_PROGRESS` | 409    | A number is already being provisioned for this agent, or this number is already being connected. Retry shortly.                                                                                                                                                                                                    |
| `NOT_FOUND`                | 404    | No number with this `id` in your workspace.                                                                                                                                                                                                                                                                        |
| `FORBIDDEN`                | 403    | The key's scope does not cover the request; `provision` and `connect-sip` need `full`.                                                                                                                                                                                                                             |
| `TELEPHONY_ERROR`          | 502    | Searching or buying numbers failed upstream: no stock for the request, the requested `phoneNumber` was taken, or the country requires regulatory documents before purchase.                                                                                                                                        |
| `VOICE_AI_ERROR`           | 502    | Registering the number for voice, or binding the agent, failed upstream. On `connect-sip` this includes a trunk configuration the voice platform rejects.                                                                                                                                                          |
| `INTERNAL_ERROR`           | 503    | Provisioning is temporarily unavailable. Nothing was bought; retry in a moment.                                                                                                                                                                                                                                    |

`5xx` responses carry the generic message `Internal server error`, including `TELEPHONY_ERROR` for a number that needs regulatory documents. If a purchase in a specific country keeps failing with `502`, try another `type` or number, or contact support.

## Limits

**Rate limits.** All `/api/phone-numbers` routes use the [general API limit](/rate-limits) for your plan.

**Scopes.** Reads need a `read` key, `PATCH` needs `write`, and `provision` and `connect-sip` need `full` because they spend money or register numbers. See [Authentication](/authentication).

**Numbers per plan.** The limit counts active voice numbers: purchased and connected numbers alike. Inactive numbers and WhatsApp-only lines do not count.

| Plan           | Numbers |
| -------------- | ------- |
| No active plan | 0       |
| Starter        | 1       |
| Growth         | 3       |
| Business       | 10      |
| Enterprise     | 9,999   |

On Starter, Growth and Business, the plan **includes one** number. Each additional purchased number is a paid add-on, so `POST /provision` can only buy a number while the workspace holds fewer than 1 plus its paid add-ons. On Enterprise the plan limit applies directly. Connecting your own number over SIP needs no add-on: it is limited only by the plan's number limit.

## Webhooks

There are no phone-number events. Calls answered on or placed from your numbers are reported with the call events, `call.completed` and `call.failed`. See [Webhooks](/webhooks) and [Calls](/resources/calls).

## Related

#### [Agents](/resources/agents)

Create the agent a number is bound to, and pause it.

#### [Calls](/resources/calls)

Place outbound calls and read inbound call records.

#### [Campaigns](/resources/campaigns)

Run voice outreach from your workspace's numbers.

#### [API reference](/api-reference)

Every phone number endpoint, parameter and response.