> This page is for Platform.

> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes.

# Coach live calls with an AI sidecar

> Attach an AI sidecar that listens to a live call and sends your server one short tip for the agent after each customer turn, then ask it questions, prompt it for a tip, and stop it while the call continues.

[make-and-receive-calls]: /docs/platform/voice/make-and-receive-calls

[prepare-calls]: /docs/platform/voice/make-and-receive-calls#prepare-for-your-first-call

[answer-swml]: /docs/platform/voice/make-and-receive-calls#answer-a-call-via-swml

[caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam

[trial-mode]: /docs/platform/trial-mode

[server-sdks]: /docs/server-sdks

[swml-intro]: /docs/swml

[ai-sidecar]: /docs/swml/reference/calling/ai-sidecar

[ai-sidecar-swaig]: /docs/swml/reference/calling/ai-sidecar/swaig

[callback-types]: /docs/swml/reference/calling/ai-sidecar#callback-types

[swml-connect]: /docs/swml/reference/calling/connect

[call-commands]: /docs/apis/rest/calls/call-commands

An AI sidecar listens to a live call between a customer and your agent, and after each customer turn
it sends your server one short tip for the agent. It never speaks on the call, so the customer hears
only your agent. Start by coaching a call between two phones you own, then ask the sidecar a
question mid-call, prompt it for a tip right away, and stop it while the call carries on.

## Pick the right product for AI sidecar

You can run a sidecar with SWML or the REST Calling API, but not every function is available on
each.

| Function                                                      | SWML | REST |
| ------------------------------------------------------------- | ---- | ---- |
| Attach a sidecar as a call starts                             |      |      |
| Attach a sidecar to a call that's already in progress         |      |      |
| Give the sidecar tools that run on your server                |      |      |
| Receive the sidecar's tips at a webhook                       |      |      |
| Ask the sidecar a question during the call                    |      |      |
| Prompt the sidecar for a tip without waiting for the customer |      |      |
| Stop the sidecar while the call continues                     |      |      |

* [SWML][swml-intro]: the call's document attaches the sidecar with [`ai_sidecar`][ai-sidecar],
  then bridges the customer to your agent. Use it when every call to a number should be coached
  from the start.
* [REST Calling API][call-commands]: commands sent to a live call by its ID, from any process.
  Use it to coach a call that's already connected, such as one your agent picked up from a queue,
  and to talk to or stop a sidecar however it was attached.

Both send their tips to the same [server](#run-the-sidecar-server), and you can mix them on one
call: attach the sidecar with SWML, then ask it questions and stop it over REST.

## Prepare for AI sidecar

Start with working call setup from [Make and receive calls][make-and-receive-calls]. To test a
sidecar you need two phones: one to call in as the customer, and one to answer as the agent. You also
need a machine that can run a small server, and a tunnel such as [ngrok](https://ngrok.com/) that
gives it a public HTTPS URL.

> **Trial projects only call and receive calls from verified numbers**
>
> A [trial project][trial-mode] only places calls to and receives calls from numbers you've
> purchased or verified. [Prepare for your first call][prepare-calls] shows how to verify the
> phones you'll test with.

Replace these values in the samples you run:

| Value                        | Replace with                                                                                    |
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
| `<YOUR_SPACE>`               | Your Space's subdomain in `<YOUR_SPACE>.signalwire.com`                                         |
| `<YOUR_PROJECT_ID>`          | Your Project ID                                                                                 |
| `<YOUR_API_TOKEN>`           | Your API token with the **Voice** permission, used only in server code                          |
| `<YOUR_PUBLIC_HOST>`         | The public hostname of your server's tunnel, such as `abc123.ngrok.app`                         |
| `<YOUR_BASIC_AUTH_PASSWORD>` | A password you choose for your server, letters and digits only, because it appears inside a URL |
| `<YOUR_CALLER_ID>`           | Your SignalWire phone number, or a [verified caller ID][caller-id], in E.164 format             |
| `<YOUR_AGENT_NUMBER>`        | The phone the agent answers, in E.164 format                                                    |
| `<YOUR_CALL_ID>`             | The ID of a live call, which your server prints when a sidecar starts                           |

The REST requests on this page go to `https://<YOUR_SPACE>.signalwire.com/api/calling/calls` and
authenticate with basic auth, using your Project ID as the username and your API token as the
password. Each request names its call in `id`, so replace the example ID with `<YOUR_CALL_ID>`.

## How the AI sidecar works

A sidecar runs alongside the call without taking part in it:

1. **The sidecar attaches to the call.** It hears both sides, and the call carries on as normal.
   A call runs one sidecar at a time.
2. **The customer finishes a turn.** The sidecar sends the conversation so far to an AI model,
   together with your prompt. The model writes one tip for the agent, or calls the built-in
   `sidecar_skip` tool to stay quiet when no tip is needed.
3. **SignalWire posts each step to your server.** Tips, skips, answers to your questions, and the
   sidecar's other steps each arrive as a callback at the URL you set.
4. **The sidecar stops.** It stops when the call ends or when you stop it, and sends a last callback
   with a record of the session.

## Run the sidecar server

Every task in this guide uses one server, which answers the sidecar's tool calls at `/swaig` and
prints its callbacks as they arrive at `/events`.

The [Server SDKs][server-sdks] handle both with `SWMLService`:

#### Python — SWMLService

```python
# Install: python -m pip install signalwire-sdk==3.4.1
# Save as sidecar_server.py and run: python sidecar_server.py
from signalwire import FunctionResult, SWMLService

service = SWMLService(
    name="sidecar-server",
    basic_auth=("signalwire", "<YOUR_BASIC_AUTH_PASSWORD>"),
)

# Stands in for your CRM.
ACCOUNTS = {"4417": "Globex: 120 seats, renews in 30 days, currently comparing ACME."}


# The sidecar calls lookup_account on /swaig.
def lookup_account(args, raw_data):
    return FunctionResult(ACCOUNTS.get(args["customer_id"], "No account matches that ID."))


service.define_tool(
    name="lookup_account",
    description="Look up an account record.",
    parameters={
        "type": "object",
        "properties": {"customer_id": {"type": "string"}},
        "required": ["customer_id"],
    },
    handler=lookup_account,
)


# The sidecar posts its callbacks to /events.
def on_event(body, headers):
    event = body.get("sidecar_event", {})  # transcription events have none
    if event.get("type") == "start":
        print(f"Sidecar started on call {event['channel_data']['call_id']}")
    elif event.get("type") == "tool_call" and event["name"] != "sidecar_skip":
        print(f"Tool call: {event['name']} {event.get('arguments', '')}")
    elif event.get("type") == "insight":
        print(f"Tip: {event['raw']}")
    elif event.get("type") == "ask_answer":
        print(f"Answer: {event['raw']}")
    elif event.get("type") == "final":
        print(f"Sidecar finished: {event['stop_reason']}")


service.register_routing_callback(on_event, path="/events")
service.serve(host="0.0.0.0", port=8080)
```

#### TypeScript — SWMLService

```typescript
// Install: npm install @signalwire/sdk@2.0.5
// Save as sidecar-server.mts and run: npx tsx sidecar-server.mts
import { FunctionResult, SWMLService } from "@signalwire/sdk";

interface SidecarEvent {
  type: string;
  channel_data: { call_id: string };
  name?: string;
  arguments?: string;
  raw?: string;
  stop_reason?: string;
}

const service = new SWMLService({
  name: "sidecar-server",
  basicAuth: ["signalwire", "<YOUR_BASIC_AUTH_PASSWORD>"],
});

// Stands in for your CRM.
const ACCOUNTS = new Map([["4417", "Globex: 120 seats, renews in 30 days, currently comparing ACME."]]);

// The sidecar calls lookup_account on /swaig. Return a plain object, which the
// SDK sends back as the tool's reply.
service.defineTool({
  name: "lookup_account",
  description: "Look up an account record.",
  parameters: {
    type: "object",
    properties: { customer_id: { type: "string" } },
    required: ["customer_id"],
  },
  handler: (args) =>
    new FunctionResult(ACCOUNTS.get(String(args.customer_id)) ?? "No account matches that ID.").toDict(),
});

// The sidecar posts its callbacks to /events. Return null to acknowledge each
// one: any other value tells the SDK to redirect the request.
service.registerRoutingCallback((body) => {
  const event = body.sidecar_event as SidecarEvent | undefined; // transcription events have none
  if (event?.type === "start") {
    console.log(`Sidecar started on call ${event.channel_data.call_id}`);
  } else if (event?.type === "tool_call" && event.name !== "sidecar_skip") {
    console.log(`Tool call: ${event.name} ${event.arguments ?? ""}`);
  } else if (event?.type === "insight") {
    console.log(`Tip: ${event.raw}`);
  } else if (event?.type === "ask_answer") {
    console.log(`Answer: ${event.raw}`);
  } else if (event?.type === "final") {
    console.log(`Sidecar finished: ${event.stop_reason}`);
  }
  return null;
}, "/events");

await service.run({ port: 8080 });
```

Start a tunnel to port `8080`, such as `ngrok http 8080`, so SignalWire can reach the server.

Each callback carries its event under a `sidecar_event` key, with a `type` that says what happened.
The same URL also receives transcription events that have no `sidecar_event` key, so the server
skips those. The main types are:

| `type`       | Sent when                                        | Field                  |
| ------------ | ------------------------------------------------ | ---------------------- |
| `start`      | The sidecar attaches to the call                 | `channel_data.call_id` |
| `tool_call`  | The model calls one of your tools                | `name`, `arguments`    |
| `insight`    | The sidecar has a tip for the agent              | `raw`                  |
| `skip`       | The sidecar stays quiet because no tip is needed | `reason`               |
| `ask_answer` | The sidecar answers a question you asked         | `raw`, `ask_id`        |
| `final`      | The sidecar stops                                | `stop_reason`          |

For a full list of callback types, see the [callback catalog][callback-types].

## Attach a sidecar to a call

A sidecar can join a call as it starts, from the call's SWML, or join a call that's already
connected, from the REST Calling API. Either way it reports to the server you just started.

### Attach a sidecar via SWML

[`ai_sidecar`][ai-sidecar] attaches the sidecar and the document moves straight on, here to a
[`connect`][swml-connect] that bridges the customer to the agent.

Create a SWML Script that holds this document, so SignalWire runs it for calls to your number. The
sales agent is being coached from the moment the call is answered:

#### Create a SWML Script

Open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. Name it
**AI sidecar** and leave **Used For** set to **Calling**.

#### Paste the sidecar document

Under **Handle Calls Using**, choose **Hosted Script** and paste this document into
**Primary Script**:

#### YAML

```yaml
version: 1.0.0
sections:
  main:
    - answer: {}
    - ai_sidecar:
        prompt: "You are a real-time sales copilot. After each customer turn, give the agent one concise piece of advice or call sidecar_skip if no advice is needed."
        lang: "en-US"
        url: "https://signalwire:<YOUR_BASIC_AUTH_PASSWORD>@<YOUR_PUBLIC_HOST>/events"
        hints: ["ACME", "Globex", "FedRAMP", "SOC 2"]
        SWAIG:
          defaults:
            web_hook_url: "https://signalwire:<YOUR_BASIC_AUTH_PASSWORD>@<YOUR_PUBLIC_HOST>/swaig"
          functions:
            - function: lookup_account
              description: "Look up an account record."
              parameters:
                type: object
                properties:
                  customer_id:
                    type: string
                required:
                  - customer_id
    - connect:
        from: "<YOUR_CALLER_ID>"
        to: "<YOUR_AGENT_NUMBER>"
    - hangup: {}
```

#### JSON

```json
{
  "version": "1.0.0",
  "sections": {
    "main": [
      {
        "answer": {}
      },
      {
        "ai_sidecar": {
          "prompt": "You are a real-time sales copilot. After each customer turn, give the agent one concise piece of advice or call sidecar_skip if no advice is needed.",
          "lang": "en-US",
          "url": "https://signalwire:<YOUR_BASIC_AUTH_PASSWORD>@<YOUR_PUBLIC_HOST>/events",
          "hints": ["ACME", "Globex", "FedRAMP", "SOC 2"],
          "SWAIG": {
            "defaults": {
              "web_hook_url": "https://signalwire:<YOUR_BASIC_AUTH_PASSWORD>@<YOUR_PUBLIC_HOST>/swaig"
            },
            "functions": [
              {
                "function": "lookup_account",
                "description": "Look up an account record.",
                "parameters": {
                  "type": "object",
                  "properties": {
                    "customer_id": {
                      "type": "string"
                    }
                  },
                  "required": ["customer_id"]
                }
              }
            ]
          }
        }
      },
      {
        "connect": {
          "from": "<YOUR_CALLER_ID>",
          "to": "<YOUR_AGENT_NUMBER>"
        }
      },
      {
        "hangup": {}
      }
    ]
  }
}
```

#### Save the script

Select **Create**.

`lang` is the only required setting. Place `ai_sidecar` before `connect` so the sidecar hears the
whole conversation. `hints` help speech recognition catch the company and product names the customer
is likely to say. The [`SWAIG`][ai-sidecar-swaig] setting gives the sidecar a `lookup_account` tool:
when the customer gives their customer ID, the model calls the tool, SignalWire posts the request to
your server's `/swaig` route, and the account record shapes the sidecar's tip.

Then assign the script to your phone number:

Open **Phone Numbers**, select the number, and select **Edit Settings**. Under **Inbound Call
Settings**, select **Assign Resource**, choose the Resource, and save. To assign from the Resource
instead, open its **Addresses & Phone Numbers** tab, select **+ Add**, then **Phone Number**, and
pick a number you own.

To do both in code, see [Answer a call via SWML][answer-swml].

Call your number from the customer phone, and answer on the agent phone. As the customer, say "Hi,
this is Globex, customer ID 4417. We're also looking at ACME." and pause. The server prints the
call's ID, the tool call, and the sidecar's tip:

```text
Sidecar started on call 3a1f6b2e-8c4d-4e7a-9b1f-5d2c7e8a9f10
Tool call: lookup_account {"customer_id":"4417"}
Tip: Globex renews 120 seats in 30 days and is comparing ACME, so ask what they need to stay.
```

When you hang up, the server prints `Sidecar finished: transcribe_close`.

If the call connects but the server prints nothing, the sidecar didn't attach. The call connects
either way, without an error, so check the document in the script, that the tunnel is running, and
the password in `url`. If `Sidecar started` appears but no `Tool call` follows, SignalWire can't
reach your server's `/swaig` route: check the host and password in `web_hook_url`.

### Attach a sidecar via REST

Send `calling.ai_sidecar` with the call's ID to attach a sidecar to a call that's already connected.
`params` takes the same settings as the SWML method. This request copies `prompt`, `lang`, `url`,
`hints`, and `SWAIG` from the document in [Attach a sidecar via SWML](#attach-a-sidecar-via-swml),
so the sidecar reports to your server and can call your tool.

A call runs one sidecar at a time, and the request fails while another sidecar is attached. A call
from the previous section already has one, so to try this request on it,
[stop that sidecar](#stop-the-sidecar-while-the-call-continues) first, then send the request with
the same call ID:

**`cURL — Calling API`**

```bash title="cURL — Calling API"
curl -X POST "https://<YOUR_SPACE>.signalwire.com/api/calling/calls" \
  -u "<YOUR_PROJECT_ID>:<YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "command": "calling.ai_sidecar",
    "id": "<YOUR_CALL_ID>",
    "params": {
      "prompt": "You are a real-time sales copilot. After each customer turn, give the agent one concise piece of advice or call sidecar_skip if no advice is needed.",
      "lang": "en-US",
      "url": "https://signalwire:<YOUR_BASIC_AUTH_PASSWORD>@<YOUR_PUBLIC_HOST>/events",
      "hints": ["ACME", "Globex", "FedRAMP", "SOC 2"],
      "SWAIG": {
        "defaults": {
          "web_hook_url": "https://signalwire:<YOUR_BASIC_AUTH_PASSWORD>@<YOUR_PUBLIC_HOST>/swaig"
        },
        "functions": [
          {
            "function": "lookup_account",
            "description": "Look up an account record.",
            "parameters": {
              "type": "object",
              "properties": {
                "customer_id": { "type": "string" }
              },
              "required": ["customer_id"]
            }
          }
        ]
      }
    }
  }'
```

For every setting the command takes, see [Call commands][call-commands].

The server prints `Sidecar started` with the call's ID, and tips follow after the customer's next
turn.

## Ask the sidecar a question during the call

An agent who wants a direct answer, rather than waiting for the next tip, can ask the sidecar a
question. The sidecar answers from what it has heard so far and can call your tools to do it. The
question doesn't become part of the sidecar's conversation, so later tips aren't affected.

### Ask the sidecar a question via REST

Send `calling.ai_sidecar.ask` with the call's ID and the question in `text`:

### Request

POST https\://%7BYour_Space_Name%7D.signalwire.com/api/calling/calls

**`calling.ai_sidecar.ask`**

```curl calling.ai_sidecar.ask
curl -X POST https://{your_space_name}.signalwire.com/api/calling/calls \
     -H "Content-Type: application/json" \
     -u "<project_id>:<api_token>" \
     -d '{
  "command": "calling.ai_sidecar.ask",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": {
    "text": "What objections has the customer raised so far?"
  }
}'
```

**`calling.ai_sidecar.ask`**

```python calling.ai_sidecar.ask
import requests

url = "https://{your_space_name}.signalwire.com/api/calling/calls"

payload = {
    "command": "calling.ai_sidecar.ask",
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "params": { "text": "What objections has the customer raised so far?" }
}
headers = {
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers, auth=("<project_id>", "<api_token>"))

print(response.json())
```

**`calling.ai_sidecar.ask`**

```javascript calling.ai_sidecar.ask
const url = 'https://{your_space_name}.signalwire.com/api/calling/calls';
const credentials = btoa("<project_id>:<api_token>");

const options = {
  method: 'POST',
  headers: {
    Authorization: `Basic ${credentials}`,
    'Content-Type': 'application/json'
  },
  body: '{"command":"calling.ai_sidecar.ask","id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","params":{"text":"What objections has the customer raised so far?"}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

**`calling.ai_sidecar.ask`**

```go calling.ai_sidecar.ask
package main

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

func main() {

	url := "https://{your_space_name}.signalwire.com/api/calling/calls"

	payload := strings.NewReader("{\n  \"command\": \"calling.ai_sidecar.ask\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"What objections has the customer raised so far?\"\n  }\n}")

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

	req.SetBasicAuth("<project_id>", "<api_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))

}
```

**`calling.ai_sidecar.ask`**

```ruby calling.ai_sidecar.ask
require 'uri'
require 'net/http'

url = URI("https://{your_space_name}.signalwire.com/api/calling/calls")

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

request = Net::HTTP::Post.new(url)
request.basic_auth("<project_id>", "<api_token>")
request["Content-Type"] = 'application/json'
request.body = "{\n  \"command\": \"calling.ai_sidecar.ask\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"What objections has the customer raised so far?\"\n  }\n}"

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

**`calling.ai_sidecar.ask`**

```java calling.ai_sidecar.ask
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://{your_space_name}.signalwire.com/api/calling/calls")
  .basicAuth("<project_id>", "<api_token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"command\": \"calling.ai_sidecar.ask\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"What objections has the customer raised so far?\"\n  }\n}")
  .asString();
```

**`calling.ai_sidecar.ask`**

```php calling.ai_sidecar.ask
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://{your_space_name}.signalwire.com/api/calling/calls', [
  'body' => '{
  "command": "calling.ai_sidecar.ask",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": {
    "text": "What objections has the customer raised so far?"
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
  ],
    'auth' => ['<project_id>', '<api_token>'],
]);

echo $response->getBody();
```

**`calling.ai_sidecar.ask`**

```csharp calling.ai_sidecar.ask
using RestSharp;
using RestSharp.Authenticators;

var client = new RestClient("https://{your_space_name}.signalwire.com/api/calling/calls");
client.Authenticator = new HttpBasicAuthenticator("<project_id>", "<api_token>");
var request = new RestRequest(Method.POST);

request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"command\": \"calling.ai_sidecar.ask\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"What objections has the customer raised so far?\"\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`calling.ai_sidecar.ask`**

```swift calling.ai_sidecar.ask
import Foundation

let credentials = Data("<project_id>:<api_token>".utf8).base64EncodedString()

let headers = [
  "Authorization": "Basic \(credentials)",
  "Content-Type": "application/json"
]
let parameters = [
  "command": "calling.ai_sidecar.ask",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": ["text": "What objections has the customer raised so far?"]
] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://{your_space_name}.signalwire.com/api/calling/calls")! 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()
```

The request returns as soon as the question is queued, and the answer arrives at your server as an
`ask_answer` callback, which the server prints after `Answer:`. Every callback for one question
carries the same `ask_id`, so you can match an answer to the question you sent. Answers reach your
server at the `url` the sidecar was started with, so attach the sidecar with one.

## Prompt the sidecar for a tip right away

When something happens that the sidecar can't hear, such as a note from a supervisor or a change in
your CRM, you can tell the sidecar about it and have it respond immediately instead of waiting for
the customer's next turn.

### Prompt the sidecar for a tip via REST

Send `calling.ai_sidecar.poke` with the call's ID and a message in `text`:

### Request

POST https\://%7BYour_Space_Name%7D.signalwire.com/api/calling/calls

**`calling.ai_sidecar.poke`**

```curl calling.ai_sidecar.poke
curl -X POST https://{your_space_name}.signalwire.com/api/calling/calls \
     -H "Content-Type: application/json" \
     -u "<project_id>:<api_token>" \
     -d '{
  "command": "calling.ai_sidecar.poke",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": {
    "text": "The customer just mentioned a competitor — suggest a comparison."
  }
}'
```

**`calling.ai_sidecar.poke`**

```python calling.ai_sidecar.poke
import requests

url = "https://{your_space_name}.signalwire.com/api/calling/calls"

payload = {
    "command": "calling.ai_sidecar.poke",
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "params": { "text": "The customer just mentioned a competitor — suggest a comparison." }
}
headers = {
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers, auth=("<project_id>", "<api_token>"))

print(response.json())
```

**`calling.ai_sidecar.poke`**

```javascript calling.ai_sidecar.poke
const url = 'https://{your_space_name}.signalwire.com/api/calling/calls';
const credentials = btoa("<project_id>:<api_token>");

const options = {
  method: 'POST',
  headers: {
    Authorization: `Basic ${credentials}`,
    'Content-Type': 'application/json'
  },
  body: '{"command":"calling.ai_sidecar.poke","id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","params":{"text":"The customer just mentioned a competitor — suggest a comparison."}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

**`calling.ai_sidecar.poke`**

```go calling.ai_sidecar.poke
package main

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

func main() {

	url := "https://{your_space_name}.signalwire.com/api/calling/calls"

	payload := strings.NewReader("{\n  \"command\": \"calling.ai_sidecar.poke\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"The customer just mentioned a competitor — suggest a comparison.\"\n  }\n}")

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

	req.SetBasicAuth("<project_id>", "<api_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))

}
```

**`calling.ai_sidecar.poke`**

```ruby calling.ai_sidecar.poke
require 'uri'
require 'net/http'

url = URI("https://{your_space_name}.signalwire.com/api/calling/calls")

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

request = Net::HTTP::Post.new(url)
request.basic_auth("<project_id>", "<api_token>")
request["Content-Type"] = 'application/json'
request.body = "{\n  \"command\": \"calling.ai_sidecar.poke\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"The customer just mentioned a competitor — suggest a comparison.\"\n  }\n}"

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

**`calling.ai_sidecar.poke`**

```java calling.ai_sidecar.poke
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://{your_space_name}.signalwire.com/api/calling/calls")
  .basicAuth("<project_id>", "<api_token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"command\": \"calling.ai_sidecar.poke\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"The customer just mentioned a competitor — suggest a comparison.\"\n  }\n}")
  .asString();
```

**`calling.ai_sidecar.poke`**

```php calling.ai_sidecar.poke
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://{your_space_name}.signalwire.com/api/calling/calls', [
  'body' => '{
  "command": "calling.ai_sidecar.poke",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": {
    "text": "The customer just mentioned a competitor — suggest a comparison."
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
  ],
    'auth' => ['<project_id>', '<api_token>'],
]);

echo $response->getBody();
```

**`calling.ai_sidecar.poke`**

```csharp calling.ai_sidecar.poke
using RestSharp;
using RestSharp.Authenticators;

var client = new RestClient("https://{your_space_name}.signalwire.com/api/calling/calls");
client.Authenticator = new HttpBasicAuthenticator("<project_id>", "<api_token>");
var request = new RestRequest(Method.POST);

request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"command\": \"calling.ai_sidecar.poke\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"params\": {\n    \"text\": \"The customer just mentioned a competitor — suggest a comparison.\"\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`calling.ai_sidecar.poke`**

```swift calling.ai_sidecar.poke
import Foundation

let credentials = Data("<project_id>:<api_token>".utf8).base64EncodedString()

let headers = [
  "Authorization": "Basic \(credentials)",
  "Content-Type": "application/json"
]
let parameters = [
  "command": "calling.ai_sidecar.poke",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": ["text": "The customer just mentioned a competitor — suggest a comparison."]
] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://{your_space_name}.signalwire.com/api/calling/calls")! 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()
```

The sidecar responds the way it does after a customer turn: the server prints a new `Tip:`, or
receives a `skip` callback when the model decides no tip is needed. The message stays in the
sidecar's conversation, so its later tips take it into account.

## Stop the sidecar while the call continues

Stopping a sidecar ends the coaching but leaves the customer and the agent connected. It works on
any sidecar, including one attached with SWML.

### Stop a sidecar via REST

Send `calling.ai_sidecar.stop` with the ID the server printed when the sidecar started:

### Request

POST https\://%7BYour_Space_Name%7D.signalwire.com/api/calling/calls

**`calling.ai_sidecar.stop`**

```curl calling.ai_sidecar.stop
curl -X POST https://{your_space_name}.signalwire.com/api/calling/calls \
     -H "Content-Type: application/json" \
     -u "<project_id>:<api_token>" \
     -d '{
  "command": "calling.ai_sidecar.stop",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "params": {}
}'
```

**`calling.ai_sidecar.stop`**

```python calling.ai_sidecar.stop
import requests

url = "https://{your_space_name}.signalwire.com/api/calling/calls"

payload = {
    "command": "calling.ai_sidecar.stop",
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
headers = {
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers, auth=("<project_id>", "<api_token>"))

print(response.json())
```

**`calling.ai_sidecar.stop`**

```javascript calling.ai_sidecar.stop
const url = 'https://{your_space_name}.signalwire.com/api/calling/calls';
const credentials = btoa("<project_id>:<api_token>");

const options = {
  method: 'POST',
  headers: {
    Authorization: `Basic ${credentials}`,
    'Content-Type': 'application/json'
  },
  body: '{"command":"calling.ai_sidecar.stop","id":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

**`calling.ai_sidecar.stop`**

```go calling.ai_sidecar.stop
package main

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

func main() {

	url := "https://{your_space_name}.signalwire.com/api/calling/calls"

	payload := strings.NewReader("{\n  \"command\": \"calling.ai_sidecar.stop\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n}")

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

	req.SetBasicAuth("<project_id>", "<api_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))

}
```

**`calling.ai_sidecar.stop`**

```ruby calling.ai_sidecar.stop
require 'uri'
require 'net/http'

url = URI("https://{your_space_name}.signalwire.com/api/calling/calls")

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

request = Net::HTTP::Post.new(url)
request.basic_auth("<project_id>", "<api_token>")
request["Content-Type"] = 'application/json'
request.body = "{\n  \"command\": \"calling.ai_sidecar.stop\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n}"

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

**`calling.ai_sidecar.stop`**

```java calling.ai_sidecar.stop
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://{your_space_name}.signalwire.com/api/calling/calls")
  .basicAuth("<project_id>", "<api_token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"command\": \"calling.ai_sidecar.stop\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n}")
  .asString();
```

**`calling.ai_sidecar.stop`**

```php calling.ai_sidecar.stop
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://{your_space_name}.signalwire.com/api/calling/calls', [
  'body' => '{
  "command": "calling.ai_sidecar.stop",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}',
  'headers' => [
    'Content-Type' => 'application/json',
  ],
    'auth' => ['<project_id>', '<api_token>'],
]);

echo $response->getBody();
```

**`calling.ai_sidecar.stop`**

```csharp calling.ai_sidecar.stop
using RestSharp;
using RestSharp.Authenticators;

var client = new RestClient("https://{your_space_name}.signalwire.com/api/calling/calls");
client.Authenticator = new HttpBasicAuthenticator("<project_id>", "<api_token>");
var request = new RestRequest(Method.POST);

request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"command\": \"calling.ai_sidecar.stop\",\n  \"id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

**`calling.ai_sidecar.stop`**

```swift calling.ai_sidecar.stop
import Foundation

let credentials = Data("<project_id>:<api_token>".utf8).base64EncodedString()

let headers = [
  "Authorization": "Basic \(credentials)",
  "Content-Type": "application/json"
]
let parameters = [
  "command": "calling.ai_sidecar.stop",
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
] as [String : Any]

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

let request = NSMutableURLRequest(url: NSURL(string: "https://{your_space_name}.signalwire.com/api/calling/calls")! 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()
```

The server prints `Sidecar finished: api_stop`, and the call stays connected.

## Next steps

#### [ai_sidecar reference](/docs/swml/reference/calling/ai-sidecar)

Every setting, the full callback catalog, and the actions a tool can return.

#### [Sidecar options](/docs/swml/reference/calling/ai-sidecar/params)

Change the model, turn timing, and end-of-call summaries.

#### [AI sidecar SWAIG](/docs/swml/reference/calling/ai-sidecar/swaig)

Give the sidecar tools and connect MCP servers.

#### [Call commands](/docs/apis/rest/calls/call-commands)

Every command you can send to a live call, including the sidecar commands.