> For a complete page index, fetch https://docs.synthflow.ai/llms.txt. For full documentation content, fetch https://docs.synthflow.ai/llms-full.txt.

# Route SIP Calls

> Route calls from your PBX, SBC, or Twilio to a Synthflow agent over SIP, either by dialing the agent's number directly or by handing off a call that is already live.

Your phone system can send a call to a Synthflow agent with a SIP `INVITE`. Use this when your system, not a SIP trunk, decides which calls reach the agent. To trunk a whole carrier or PBX into Synthflow instead, see [Connect over SIP](/connect-over-sip).

| Method                                  | Use it when                                                                                                                      |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [Direct dialing](#direct-dialing)       | Your system sends a new inbound call straight to the agent.                                                                      |
| [Live call handoff](#live-call-handoff) | Your system connects the call first, for example to run its own voicemail detection, then passes it to the agent with variables. |

## Prerequisites

* A [custom number](/phone-numbers#adding-custom-numbers) assigned to an inbound agent. Synthflow treats every call to that number as inbound.
* Firewall rules for your region's [signaling and media addresses](/ip-allow-lists#region-addresses).
* An [API key](/authentication), to hand off a live call or to set up the number through the API.

Send every `INVITE` to your region's SIP address on port `32681`, listed in [SIP addresses](/connect-over-sip#sip-addresses). Format the number in E.164, for example `sip:+12065551234@sip.us.synthflow.ai:32681`.

## API setup

You can create the custom number and the agent in the app, or with two API calls.

First, import the number. If your carrier or PBX requires Synthflow to register, set `uac_enabled` to `true` and provide `trunk_username` and `trunk_pwd`.

### Request

POST [https://api.synthflow.ai/v2/custom-numbers](https://api.synthflow.ai/v2/custom-numbers)

**`Import Custom Number`**

```curl Import Custom Number
curl -X POST https://api.synthflow.ai/v2/custom-numbers \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "workspace_id": "1710107690998x536152705164378100",
  "phone_number": "+33782990580",
  "friendly_name": "My Custom Number",
  "provider_name": "custom",
  "termination_uri": "sip.example.com"
}'
```

**`Import Custom Number`**

```python Import Custom Number
import requests

url = "https://api.synthflow.ai/v2/custom-numbers"

payload = {
    "workspace_id": "1710107690998x536152705164378100",
    "phone_number": "+33782990580",
    "friendly_name": "My Custom Number",
    "provider_name": "custom",
    "termination_uri": "sip.example.com"
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

**`Import Custom Number`**

```go Import Custom Number
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.synthflow.ai/v2/custom-numbers"

	payload := strings.NewReader("{\n  \"workspace_id\": \"1710107690998x536152705164378100\",\n  \"phone_number\": \"+33782990580\",\n  \"friendly_name\": \"My Custom Number\",\n  \"provider_name\": \"custom\",\n  \"termination_uri\": \"sip.example.com\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

**`Import Custom Number`**

```ruby Import Custom Number
require 'uri'
require 'net/http'

url = URI("https://api.synthflow.ai/v2/custom-numbers")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"workspace_id\": \"1710107690998x536152705164378100\",\n  \"phone_number\": \"+33782990580\",\n  \"friendly_name\": \"My Custom Number\",\n  \"provider_name\": \"custom\",\n  \"termination_uri\": \"sip.example.com\"\n}"

response = http.request(request)
puts response.read_body
```

**`Import Custom Number`**

```java Import Custom Number
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.synthflow.ai/v2/custom-numbers")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"workspace_id\": \"1710107690998x536152705164378100\",\n  \"phone_number\": \"+33782990580\",\n  \"friendly_name\": \"My Custom Number\",\n  \"provider_name\": \"custom\",\n  \"termination_uri\": \"sip.example.com\"\n}")
  .asString();
```

**`Import Custom Number`**

```csharp Import Custom Number
using RestSharp;

var client = new RestClient("https://api.synthflow.ai/v2/custom-numbers");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"workspace_id\": \"1710107690998x536152705164378100\",\n  \"phone_number\": \"+33782990580\",\n  \"friendly_name\": \"My Custom Number\",\n  \"provider_name\": \"custom\",\n  \"termination_uri\": \"sip.example.com\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`Import Custom Number`**

```swift Import Custom Number
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "workspace_id": "1710107690998x536152705164378100",
  "phone_number": "+33782990580",
  "friendly_name": "My Custom Number",
  "provider_name": "custom",
  "termination_uri": "sip.example.com"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.synthflow.ai/v2/custom-numbers")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

Then create an inbound agent on that number. Wait until the number is created before you send this request.

### Request

POST [https://api.synthflow.ai/v2/assistants](https://api.synthflow.ai/v2/assistants)

```curl
curl -X POST https://api.synthflow.ai/v2/assistants \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "type": "outbound",
  "name": "Sales Assistant",
  "agent": {
    "prompt": "You are a helpful sales assistant for Acme Corp.",
    "greeting_message": "Hello, this is Sarah from Acme Corp. How can I help you today?",
    "llm": "gpt-4.1-Mini",
    "language": "en-US",
    "voice_id": "eleven_turbo_v2"
  }
}'
```

```python
import requests

url = "https://api.synthflow.ai/v2/assistants"

payload = {
    "type": "outbound",
    "name": "Sales Assistant",
    "agent": {
        "prompt": "You are a helpful sales assistant for Acme Corp.",
        "greeting_message": "Hello, this is Sarah from Acme Corp. How can I help you today?",
        "llm": "gpt-4.1-Mini",
        "language": "en-US",
        "voice_id": "eleven_turbo_v2"
    }
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.synthflow.ai/v2/assistants"

	payload := strings.NewReader("{\n  \"type\": \"outbound\",\n  \"name\": \"Sales Assistant\",\n  \"agent\": {\n    \"prompt\": \"You are a helpful sales assistant for Acme Corp.\",\n    \"greeting_message\": \"Hello, this is Sarah from Acme Corp. How can I help you today?\",\n    \"llm\": \"gpt-4.1-Mini\",\n    \"language\": \"en-US\",\n    \"voice_id\": \"eleven_turbo_v2\"\n  }\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.synthflow.ai/v2/assistants")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"type\": \"outbound\",\n  \"name\": \"Sales Assistant\",\n  \"agent\": {\n    \"prompt\": \"You are a helpful sales assistant for Acme Corp.\",\n    \"greeting_message\": \"Hello, this is Sarah from Acme Corp. How can I help you today?\",\n    \"llm\": \"gpt-4.1-Mini\",\n    \"language\": \"en-US\",\n    \"voice_id\": \"eleven_turbo_v2\"\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.synthflow.ai/v2/assistants")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"type\": \"outbound\",\n  \"name\": \"Sales Assistant\",\n  \"agent\": {\n    \"prompt\": \"You are a helpful sales assistant for Acme Corp.\",\n    \"greeting_message\": \"Hello, this is Sarah from Acme Corp. How can I help you today?\",\n    \"llm\": \"gpt-4.1-Mini\",\n    \"language\": \"en-US\",\n    \"voice_id\": \"eleven_turbo_v2\"\n  }\n}")
  .asString();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.synthflow.ai/v2/assistants");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"type\": \"outbound\",\n  \"name\": \"Sales Assistant\",\n  \"agent\": {\n    \"prompt\": \"You are a helpful sales assistant for Acme Corp.\",\n    \"greeting_message\": \"Hello, this is Sarah from Acme Corp. How can I help you today?\",\n    \"llm\": \"gpt-4.1-Mini\",\n    \"language\": \"en-US\",\n    \"voice_id\": \"eleven_turbo_v2\"\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "type": "outbound",
  "name": "Sales Assistant",
  "agent": [
    "prompt": "You are a helpful sales assistant for Acme Corp.",
    "greeting_message": "Hello, this is Sarah from Acme Corp. How can I help you today?",
    "llm": "gpt-4.1-Mini",
    "language": "en-US",
    "voice_id": "eleven_turbo_v2"
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.synthflow.ai/v2/assistants")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

| Field          | Value                                                                       |
| -------------- | --------------------------------------------------------------------------- |
| `type`         | `inbound`                                                                   |
| `name`         | A name for the agent                                                        |
| `phone_number` | The custom number you imported                                              |
| `agent`        | The agent's `llm`, `language`, `prompt`, `greeting_message`, and `voice_id` |

## Direct dialing

Send the call to the agent's number at your region's SIP address. Synthflow answers it like any other inbound call.

To keep a reference to the call in your own system, add your call ID in the `X-EI` header. The example below redirects a Twilio call with TwiML and passes the Twilio Call SID.

**`Node.js`**

```javascript Node.js
const express = require('express');
const { twiml: { VoiceResponse } } = require('twilio');

const app = express();
const SYNTHFLOW_SIP = 'sip.us.synthflow.ai:32681';

app.get('/redirect_call', (req, res) => {
  const { Called, CallSid } = req.query;
  if (!Called) {
    return res.status(400).send('Missing "Called" query parameter');
  }

  const response = new VoiceResponse();
  response.dial().sip(`sip:${Called}@${SYNTHFLOW_SIP}?X-EI=${CallSid}`);
  res.type('text/xml').send(response.toString());
});

app.listen(process.env.PORT || 3000);
```

**`Python`**

```python Python
from fastapi import FastAPI, Response
from twilio.twiml.voice_response import VoiceResponse

app = FastAPI()
SYNTHFLOW_SIP = "sip.us.synthflow.ai:32681"

@app.get("/redirect_call")
async def redirect_call(Called: str, CallSid: str):
    response = VoiceResponse()
    response.dial().sip(f"sip:{Called}@{SYNTHFLOW_SIP}?X-EI={CallSid}")
    return Response(str(response), media_type="text/xml")
```

A [before-the-call action](/pass-call-context#before-the-call-actions) can send `<twilio_call_sid>` to your API, so the agent starts the call with data from your system.

## Live call handoff

Your system places or answers the call, runs its own logic, and then transfers the connected call to the agent. Register the call with Synthflow first, so the agent receives your variables and the call keeps one ID.

```mermaid
flowchart LR
    A[Your system connects the caller] --> B[POST /v2/prepare_inbound]
    B --> C[SIP INVITE with X-EI header]
    C --> D[Synthflow agent continues the call]
```

### 1. Call registration

Send a `POST` to `/v2/prepare_inbound` on your region's [API base URL](/getting-started-with-your-api#base-urls), with your API key as a bearer token.

```bash
curl -X POST https://api.us.synthflow.ai/v2/prepare_inbound \
  -H "Authorization: Bearer $SYNTHFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_number": "+14155550199",
    "to_number": "+12065551234",
    "model_id": "your-agent-id",
    "workspace_id": "your-workspace-id",
    "custom_variables": {
      "customer_name": "Jane Doe",
      "account_id": "A12345"
    }
  }'
```

| Field              | Description                                       |
| ------------------ | ------------------------------------------------- |
| `from_number`      | The caller's number, in E.164 format.             |
| `to_number`        | The called number, in E.164 format.               |
| `model_id`         | The agent ID of the inbound agent.                |
| `workspace_id`     | Your workspace ID.                                |
| `custom_variables` | Key-value pairs passed to the agent as variables. |

The response includes two fields you need:

```json
{
  "call_status": "registered",
  "synthflow_call_id": "ebe4bd96-dcb1-4abd-9aa5-686877dab061"
}
```

* `call_status` is `registered` when Synthflow is ready to take the call, and `failed` during a service outage.
* `synthflow_call_id` identifies the call. You send it in the next step.

### 2. SIP transfer

Once the caller is connected, send an `INVITE` to the agent's number at your region's SIP address. Add an `X-EI` header with the call ID from step 1, in the exact format `S.<synthflow_call_id>;`, including the trailing semicolon.

The number must be the one assigned to the inbound agent, in E.164 format.

**`SIP`**

```text SIP
INVITE sip:+12065551234@sip.us.synthflow.ai:32681 SIP/2.0
X-EI: S.ebe4bd96-dcb1-4abd-9aa5-686877dab061;
```

**`Asterisk`**

```text Asterisk
same => n,Set(PJSIP_HEADER(add,X-EI)=S.<synthflow_call_id>;)
same => n,Transfer(SIP/+12065551234@sip.us.synthflow.ai:32681)
```

**`Twilio (Node.js)`**

```javascript Twilio (Node.js)
const response = new VoiceResponse();
response.dial().sip(
  `sip:+12065551234@sip.us.synthflow.ai:32681?X-EI=S.${synthflowCallId};`
);
```

`X-EI` can also carry your own call ID and the original caller. See [X-EI header](/pass-call-context#x-ei-header) for the full format.

## Additional context

Any `X-*` header you add to the `INVITE` becomes a variable the agent can use in its prompt, actions, and transfers. See [Pass call context](/pass-call-context).