> ## Documentation Index
> Fetch the complete documentation index at: https://docs.60db.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Bring your own SIP

> Make and receive agent calls on your own carrier or PBX with Phone lines (SIP trunks): setup, firewall, number format, recipes and troubleshooting

## Overview

A **phone line** of kind `byo` connects an [agent](https://docs.60db.ai/features/agents.md) to your own carrier or PBX over SIP. Your carrier bills the minutes; 60db bills the AI part (speech, LLM, memory) from your AI credits as for any call.

```
 caller ──► your carrier / PBX ──SIP INVITE (UDP)──► 60db  ──► agent (voice, script, tools)
                                 sip:+917965318709@hellokit-sip.60db.ai:5060

 agent  ──► 60db ──SIP INVITE (UDP)──► your carrier / PBX ──► the person you call
```

* **Inbound.** Your carrier or PBX sends the call to `hellokit-sip.60db.ai`. The agent attached to the called number answers.
* **Outbound.** [Place a call](https://docs.60db.ai/api-reference/agents/place-call.md) with a `byo` line. 60db dials your carrier's `domain` and the agent talks.

Phone lines appear in the dashboard as **Phone lines** with the subtitle **SIP trunk**. If you are coming from LiveKit or Pipecat, read [Move from LiveKit or Pipecat](https://docs.60db.ai/features/agents-migrate-from-livekit-pipecat.md) first. To use 60db as the speech engine inside LiveKit instead, see the [LiveKit integration](https://docs.60db.ai/integrations/livekit.md).

> [!WARNING]
>
> **Tested carriers.** Only the 60db carrier has been tested end to end against a live server. Every other carrier and PBX recipe on this page uses standard SIP and is expected to work, but is **untested**. Settings screens change, so check each against your provider's own documentation. The Kamailio and 3CX recipes are additionally marked **not yet verified end to end**.

## What 60db needs from a call

| Item | What 60db expects |
| - | - |
| Address | `hellokit-sip.60db.ai`, port **5060** |
| Transport | **UDP** only. TCP, TLS (SIPS) and WebSocket are not supported |
| Called number | In the **user part of the request URI**, in full international form: `sip:+917965318709@hellokit-sip.60db.ai:5060`. It must equal the line's `inbound_number` (or one of its `numbers`) exactly, `+` and country code included, unless you turn on `normalize_numbers` |
| Audio | **G.711 µ-law (PCMU)** or **A-law (PCMA)** must be in the offer. 60db answers with PCMU when offered, else PCMA. An offer with neither (G.729 only, for example) is refused with `488`. Outbound offers list PCMU first, then PCMA |
| IP version | IPv4 in the SDP (`c=IN IP4 ...`) |
| Keypad tones | RFC 4733 `telephone-event` is negotiated when offered |
| Authentication | **None on the INVITE.** The source is checked by address (`allowed_addresses`), see [Firewall](#firewall-and-ports) |
| Media encryption | None. **No TLS and no SRTP**, plain RTP over UDP |

Answers 60db gives to an inbound call:

| Answer | Meaning |
| - | - |
| `200` | Answered |
| `403` | The call came from an address not in `allowed_addresses`, or the caller is not in `allowed_numbers` |
| `404` | The number is not on any line with an agent (often a number-format problem) |
| `486` | The line or the number is at its call limit |
| `488` | No usable audio (no PCMU or PCMA in the offer) |
| `503` | 60db could not start the call |

## Set it up

**1. Create an agent**

[Create an agent](https://docs.60db.ai/api-reference/agents/create-agent.md) with its script, voice and, if you need them, tools and a [knowledge base](https://docs.60db.ai/features/agents-knowledge.md). Copy its `id`

**2. Create the phone line**

`POST /agents/phone-lines` with `kind: "byo"`, the number your carrier dials as `inbound_number`, the agent, and your carrier's source addresses as `allowed_addresses`

**3. Read the setup guide**

`GET /agents/phone-lines/{id}/setup` returns what to enter at the carrier and what is still missing

**4. Dry-run it**

`POST /agents/phone-lines/{id}/check` says how an incoming call would be answered, before the carrier sends one

**5. Point the carrier at 60db**

Set the carrier's destination to `sip:+<number>@hellokit-sip.60db.ai:5060;transport=udp`, then call the number

### Create the line

```bash theme={null}
curl -X POST https://api.60db.ai/agents/phone-lines \
  -H "Authorization: Bearer $SIXTYDB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "byo",
    "name": "ip-carrier",
    "domain": "sip.carrier.example",
    "caller_id": "+15550100",
    "inbound_number": "+15550301",
    "agent_id": "9c2e1d7a-0b3f-4e6a-8f1d-5a7b9c0e2d44",
    "allowed_addresses": ["203.0.113.7", "198.51.100.0/24"],
    "allowed_numbers": ["+919876543210"]
  }'
```

No `username` or `password` is needed for a carrier that trusts your server's IP and gives no login. Send both or neither.

| Field | Meaning |
| - | - |
| `name`, `domain`, `caller_id` | As in [Create phone line](https://docs.60db.ai/api-reference/agents/create-phone-line.md). `domain` is where **outbound** calls are sent (UDP, port 5060 unless the domain gives one) |
| `username`, `password` | Optional, both or neither. Used for outbound calls to a carrier that asks for a login. The password is never returned |
| `inbound_number` | The number your carrier dials for inbound calls, E.164 with `+`. `null` for an outbound-only line |
| `agent_id` | The agent that answers `inbound_number`. `null` to detach |
| `numbers` | More numbers on the same line, up to 50: `[{ "number": "+919876500001", "agent_id": "…" }]`. A number with an `agent_id` is answered by that agent (it must be in your workspace); one with `null` is answered by the line's `agent_id`. Unique, and never equal to `inbound_number`. On update, `[]` clears the list |
| `allowed_addresses` | Source IPs or CIDR blocks (IPv4 or IPv6, up to 64) that may send calls to this line. Any other source gets `403`. On update, `[]` clears the list |
| `allowed_numbers` | Caller numbers (E.164, up to 64) that may call this line. Others get `403` |
| `normalize_numbers` | Default `false`, so matching is exact. When `true`, a called number also matches when written without `+` (`919876500001`), with `00` (`00919876500001`) or, with `default_country_code`, with a national leading `0` (`09876500001`). An exact match always wins |
| `default_country_code` | Digits, 1 to 4 characters (for example `91`), or `null`. Used by `normalize_numbers` |
| `bridge_transfers` | Default `false`. When `true`, an inbound call is handed to a person by [bridging](#transfers) instead of SIP REFER |

The rules are read once when a call arrives. Nothing is added to the audio or reply path.

> [!WARNING]
>
> **Open lines.** A `byo` line with an inbound number and an empty `allowed_addresses` is accepted, but then any source can send calls that are billed to your workspace. The response and the setup guide carry `warnings: [{ "code": "open_trunk" }]` and the dashboard shows it prominently. Set `allowed_addresses`.

## The setup guide

`GET /agents/phone-lines/{id}/setup` is read-only. It is read from the stored line and is not on any call's path.

```bash theme={null}
curl https://api.60db.ai/agents/phone-lines/LINE_ID/setup \
  -H "Authorization: Bearer $SIXTYDB_API_KEY"
```

```json theme={null}
{
  "success": true,
  "data": {
    "sip": {
      "host": "hellokit-sip.60db.ai",
      "port": 5060,
      "transport": "udp",
      "codecs": ["PCMU", "PCMA"],
      "request_uri_example": "sip:+15550301@hellokit-sip.60db.ai:5060;transport=udp"
    },
    "numbers": ["+15550301"],
    "number_format": {
      "request_uri_user_part": "E.164 with a leading +",
      "normalize_numbers": false,
      "default_country_code": null
    },
    "source_rules": { "allowed_addresses": ["203.0.113.7"], "uses_server_list": false },
    "warnings": []
  }
}
```

`warnings` is a list of `{ code, message }`:

| Code | Meaning |
| - | - |
| `no_number` | The line has no `inbound_number` and no `numbers`, so no call can reach it |
| `no_agent` | No agent is attached, so a call is refused with `404` |
| `open_trunk` | No `allowed_addresses` on the line, so any source may send calls billed to your workspace. Set `allowed_addresses` |
| `no_public_host` | The address to give the carrier is unknown |
| `inbound_disabled` | Inbound listening is not available on this environment yet |

Nothing is enforced by the guide: an open line keeps working. It only tells you.

## The dry-run check

`POST /agents/phone-lines/{id}/check` runs the same decision a real call would, without placing or receiving one.

```bash theme={null}
curl -X POST https://api.60db.ai/agents/phone-lines/LINE_ID/check \
  -H "Authorization: Bearer $SIXTYDB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "called_number": "+15550301", "source_ip": "203.0.113.7", "caller_number": "+919876543210" }'
```

```json theme={null}
{
  "success": true,
  "data": {
    "answer": 200,
    "result": "answered",
    "message": "The line's agent would answer this call.",
    "inbound_enabled": true
  }
}
```

| `answer` | `result` | Meaning |
| - | - | - |
| 200 | `answered` | The line's agent would answer |
| 404 | `number_not_on_this_trunk` | No number on this line matches. Check the form: a leading `+`, the country code, `normalize_numbers`. A number that belongs to another line also gets this, and nothing else is said about it |
| 403 | `source_not_allowed` | The source address is not in the line's `allowed_addresses` |
| 403 | `caller_not_allowed` | The caller is not in the line's `allowed_numbers` |

`inbound_enabled` is `false` when inbound calls are not available on the environment at all. The check cannot prove your carrier can reach 60db. Only a real call does.

## One number, one stack

A number can answer in exactly one place. Creating or updating a line is refused when the number is already used by any of:

* another Agents phone line (`inbound_number` or an entry in `numbers`)
* a [Dialer](https://docs.60db.ai/features/dialer.md) number
* a LiveKit realtime number

| Status | Code | Meaning |
| - | - | - |
| 409 | `NUMBER_IN_USE` | The number is already in use in **your** workspace. `field` names where |
| 409 | `NUMBER_OWNED_ELSEWHERE` | The number belongs to another workspace. `field` names where |

To move a number from one stack to another, remove it from the old one first.

## Firewall and ports

The INVITE carries no password, so anyone who can reach the SIP port can start a call that is billed to your workspace. Restrict by address with `allowed_addresses`, and restrict on your own firewall too.

| Direction | Open | Why |
| - | - | - |
| **Inbound** to 60db | Your carrier or PBX must be able to send **UDP 5060** to `hellokit-sip.60db.ai` and **UDP `40000-40199`** (60db's audio range) | The INVITE, then the audio. The carrier's media address is chosen per call |
| **Outbound** from 60db | Your carrier or PBX must accept **UDP 5060** from 60db, and your carrier's **RTP range** must accept audio from 60db | 60db dials your `domain`. Allow 60db's source addresses on your side |

> [!WARNING]
>
> **TODO (needs 60db operations, not published yet): the public source IP addresses of the hosted service.** To allow outbound calls from 60db on your firewall or carrier's IP allow-list you need them, and they are not listed here. Until they are published, ask 60db support. Do not guess or copy addresses from another provider.

* Ask your carrier for **all** the addresses its SIP servers send from. They often have several. Calls from an address that is not listed are refused with `403`.
* Close everything else. SSH only from your own address.

### NAT

* **60db side.** The hosted service advertises its public address in the SDP, so audio reaches it without any setting on your side.
* **Your side.** A PBX behind NAT must put its **public** address in the SDP it sends, and forward its RTP range to the PBX. In Asterisk this is `external_media_address`, `external_signaling_address` and `local_net`; in FreeSWITCH `ext-rtp-ip` and `ext-sip-ip`. Without it you get ringing and then one-way or no audio.
* Keep SIP ALG (a router feature that rewrites SIP) off. It commonly breaks both signalling and audio.

## Number format

Most inbound problems are the number. 60db matches the user part of the request URI.

* It must be **E.164 with a leading `+`**: `+917965318709`.
* If your carrier writes `07965318709`, `00917965318709` or `917965318709`, either change the carrier's number format or dial-pattern setting, or turn on `normalize_numbers` (and set `default_country_code` for the national form).
* Caller numbers in `allowed_numbers` and `caller_id` are E.164 too.

## Codecs

PCMU (G.711 µ-law) and PCMA (G.711 A-law), 8 kHz. Nothing else is negotiated. Put them in your trunk's codec list and remove G.729, Opus and the rest, or the call may be refused with `488`.

## Recipes

For every recipe: give the call to `sip:+<number>@hellokit-sip.60db.ai:5060;transport=udp`, keep the number in `+E.164` form, allow only PCMU and PCMA, switch off SRTP and TLS, and add the carrier's or PBX's public SIP address to the line's `allowed_addresses`.

### 60db carrier (tested)

The 60db Dialer is the only carrier tested end to end: an inbound call arrives with `INVITE sip:+917965318709@…`, offers PCMA, PCMU and `telephone-event`, and is answered with PCMU. For a 60db Dialer number you do not create a `byo` line: add the number as a `sixtydb` line. See [Create phone line](https://docs.60db.ai/api-reference/agents/create-phone-line.md).

### Twilio, Telnyx, Plivo and other cloud carriers (untested)

Use their SIP trunking product (Twilio Elastic SIP Trunking, Telnyx SIP Connection, Plivo Zentrunk):

1. Add an **origination URI** of `sip:hellokit-sip.60db.ai:5060;transport=udp` and assign your number to the trunk.
2. Make sure the provider sends the number as **E.164 with `+`** in the request URI. If it sends the national form or drops the `+`, 60db answers `404`. Most providers have a number-format or dial-pattern setting for this.
3. Add the provider's published SIP signalling addresses to the line's `allowed_addresses`. If you use the wrong list you will see `403`.
4. Do not enable SRTP or TLS media.
5. For outbound calls, create the line with the provider's termination `domain`, plus `username` and `password` for credential auth, or leave both out and allow 60db's source addresses at the provider (IP authentication). The `caller_id` must be a number the provider lets you present.

### Indian carriers and dialers (untested)

Exotel, Knowlarity, Ozonetel and similar cloud-telephony dialers usually call this "SIP trunk", "forward to SIP" or "SIP endpoint". Enter the address above, and ask the provider for (a) the source addresses to allow and (b) to send the number as `+91...`. Many send numbers without the `+`, or with a leading `0`. Fix that on their side, or use `normalize_numbers` with `default_country_code: "91"`.

### Asterisk (untested)

`pjsip.conf` and `extensions.conf`, to send the call for number `+917965318709` to the agent:

```ini theme={null}
[60db]
type=endpoint
transport=transport-udp
context=from-60db
disallow=all
allow=ulaw
allow=alaw
aors=60db

[60db]
type=aor
contact=sip:hellokit-sip.60db.ai:5060

[60db]
type=identify
endpoint=60db
match=<60db source IP address>   ; for calls coming back from 60db, such as a bridged transfer

; send an incoming call to the agent for number +917965318709
exten => _+917965318709,1,Dial(PJSIP/${EXTEN}@60db,60)
```

`<60db source IP address>` is the hosted source address that is not yet published (see the TODO above). To receive outbound calls from the agent, add a `from-60db` context in `extensions.conf` that routes the dialled number to your trunk.

### FreeSWITCH (untested)

In the dialplan, bridge the call out with the number in `+E.164` form and PCMU allowed:

```xml theme={null}
<action application="bridge" data="sofia/external/+917965318709@hellokit-sip.60db.ai:5060"/>
```

### Kamailio (not yet verified end to end)

Written from standard Kamailio configuration, not run against 60db. Kamailio stays in the signalling path only, so audio flows directly between your carrier and 60db: the SDP your carrier sends must carry a reachable address (`rtpengine` is only needed if media must be anchored).

```text theme={null}
# kamailio.cfg: send calls for a number to the 60db agent
request_route {
    if (is_method("INVITE") && $rU =~ "^\+917965318709$") {
        $ru = "sip:" + $rU + "@hellokit-sip.60db.ai:5060;transport=udp";
        record_route();
        t_on_failure("60DB_FAIL");
        if (!t_relay()) { sl_reply_error(); }
        exit;
    }
    # ... your other routing ...
}

failure_route[60DB_FAIL] {
    xlog("L_INFO", "60db returned $T_reply_code for $rU\n");
}
```

The address Kamailio sends from is the one to put in `allowed_addresses`. Use `udp` as the only listener transport toward 60db.

### 3CX (not yet verified end to end)

Written from 3CX's generic SIP trunk model, not run against 60db. Menu names differ between 3CX versions, so check your version's documentation.

1. Add a **Generic SIP Trunk (IP based)** (no registration, no login).
2. Registrar or proxy: `hellokit-sip.60db.ai`, port `5060`, transport **UDP**.
3. Codecs: PCMU and PCMA only. Turn **SRTP** and **TLS** off.
4. Add an **outbound rule** that sends the numbers (or the extension that represents the agent) to this trunk, with the number in `+E.164` form.
5. Put the 3CX server's public address in the line's `allowed_addresses`.

## Transfers

When the agent hands a caller to a person with `transfer_targets`:

* **Inbound calls, default.** 60db sends a SIP **REFER** on the call's own line, asking your side to call `sip:<number>@<your domain>`. Your carrier or PBX must support REFER on that trunk, and the trunk must be allowed to call the destination number (a trunk limited to one country cannot transfer outside it). A carrier that ignores REFER leaves the caller in silence. Test one real transfer before relying on it.
* **Inbound calls with `bridge_transfers: true`.** 60db dials the person itself, using the line's own login and caller ID, and joins the two. This works with a carrier that ignores REFER, but the carrier must let the line **place calls** (an inbound-only trunk refuses the INVITE), and the line carries both legs for as long as they talk, so you pay minutes for both.
* **Outbound calls.** Always bridged.

See [Call transfer](https://docs.60db.ai/features/agents-tools-and-webhooks.md#call-transfer). Warm transfer (the agent briefs the person first) is [rolling out](https://docs.60db.ai/features/agents-migrate-from-livekit-pipecat.md#rolling-out-features) and needs a bridge.

## Keypad tones (DTMF)

The agent can press keys on the other party's phone (RFC 4733 tones) and collect digits the caller types. The other side must offer `telephone-event` in its SDP, or pressing keys is reported as not supported. The tool names `send_dtmf` and `collect_digits` are reserved. Digits go to the model as the tool's result, so do not collect a secret the model should not see.

## Limits

* **UDP only**, **plain RTP**: no TLS and no SRTP. If your carrier requires TLS or SRTP toward its SIP target, the line does not work yet. Terminate TLS and SRTP at your own session border controller (SBC) or PBX and speak plain UDP and RTP to 60db over a path you control, or use a [stream call](https://docs.60db.ai/api-reference/agents/start-stream-call.md), which runs over HTTPS and needs no SIP.
* **PCMU and PCMA only.**
* A call past a capacity limit hears busy (`486`).
* Several calls at once are allowed on a line, up to your workspace's limits. See [Agents limits](https://docs.60db.ai/features/agents.md#limits).

## Troubleshooting

Use `POST /agents/phone-lines/{id}/check` first. It answers most "why 404 or 403" questions without a call.

| Symptom | Cause | Fix |
| - | - | - |
| Caller hears fast busy or "number not in service" (`404`) | The number in the request URI is not on any line with an agent: missing `+`, national format, wrong number | Fix the carrier's number format, or set `normalize_numbers` and `default_country_code`. Run the check |
| `403` | The source address is not in `allowed_addresses`, or the caller is not in `allowed_numbers` | Add the carrier's real sending address (they often have several). Run the check with its `source_ip` |
| `486` busy | The line or the number is at its call limit | Wait for calls to end, or reduce concurrency |
| `488` | No PCMU or PCMA in the offer | Allow G.711 on the trunk, remove G.729 and Opus |
| Ringing, then silence, or one-way audio | RTP blocked (`40000-40199` toward 60db, or your own range toward your PBX), or your PBX advertises a private address in the SDP | Open the ports, set the external address on your PBX, switch SIP ALG off |
| Agent answers but the call drops at once | SRTP or TLS media is on | Turn both off |
| Nothing reaches 60db at all | The carrier is not sending to this address | Recheck its destination setting, then capture with `sudo tcpdump -ni any udp port 5060` on your PBX |
| Outbound call fails to connect | 60db's source is not allowed by your carrier, or the login is wrong | Allow 60db's source addresses (see the TODO above), recheck `username` and `password` |
| Transfer: the agent says nobody is available | The carrier ignores REFER, or refuses the destination | Turn on `bridge_transfers` (the line must be allowed to place calls), or ask the carrier to enable REFER |
| `NUMBER_IN_USE` or `NUMBER_OWNED_ELSEWHERE` on create | The number answers in another stack | See [One number, one stack](#one-number-one-stack) |

## Related

* [Move from LiveKit or Pipecat](https://docs.60db.ai/features/agents-migrate-from-livekit-pipecat.md)
* [Knowledge for agents](https://docs.60db.ai/features/agents-knowledge.md)
* [Agents](https://docs.60db.ai/features/agents.md) and [Tools and webhooks](https://docs.60db.ai/features/agents-tools-and-webhooks.md)
* [Create phone line](https://docs.60db.ai/api-reference/agents/create-phone-line.md), [Get phone line setup](https://docs.60db.ai/api-reference/agents/get-phone-line-setup.md), [Check phone line](https://docs.60db.ai/api-reference/agents/check-phone-line.md)
* [Names: HelloKit and 60db](https://docs.60db.ai/features/agents-glossary.md)
* [Agents MCP tools](https://docs.60db.ai/mcp-server/agents.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.