Skip to navigation

Chat with an AI agent

Reach the same AI agent by text instead of a phone call
View as MarkdownOpen in Claude

The AI agent you built for voice can also hold a text conversation. Each HTTP request-response cycle represents one exchange between the user and the AI. You send the user’s message as an HTTP request, and the agent’s reply comes back in the response. An ongoing text conversation can be escalated to voice and back to text with no loss of conversation context.

Start by sending two requests to an agent saved in your Space through HTTP, then hold conversations from your server, from a web page, and over SMS text messages, move one onto a phone call and back, and receive the agent’s tool calls and summaries.

AI chat is not the Chat API

AI chat is the ai method, or a Server SDKs agent, reached over text instead of voice. The Chat API is a separate messaging product.

Prepare for AI chat

Have these values ready:

  • Your Space URL, such as <YOUR_SPACE>.signalwire.com.
  • Your Project ID and an API token from the Dashboard’s API credentials page, with the token’s Chat permission enabled.
  • An agent: a SWML document with an ai method, served at a URL SignalWire can fetch from the public internet. The first run below saves one in your Space.
  • For text messages, a messaging-capable phone number in your Space.
  • For tool calls, summaries, text messages, and the web page gateway, a publicly reachable HTTPS URL for your application.
  • To move a conversation onto a phone call, a voice-capable number in your Space.
Never put your API token in a web page

Anything a page can read, a visitor can read. An API token in page code is a published API token, so a web page reaches AI chat only through ChatGateway on your server.

Hold your first AI chat conversation

Hold a short text conversation with an agent saved in your Space, using nothing but HTTP.

1

Set your credentials

Replace these values in the code sample you choose:

ValueReplace with
<YOUR_SPACE>Your Space’s subdomain in <YOUR_SPACE>.signalwire.com
<YOUR_PROJECT_ID>Your Project ID
<YOUR_API_TOKEN>An API token with the Chat permission
<YOUR_AGENT_ID>The ID at the end of your agent’s request URL, https://<YOUR_SPACE>.signalwire.com/relay-bins/<YOUR_AGENT_ID>
<YOUR_GATEWAY_KEY>A publishable key you choose for your web page, such as pk_bayview_web
<YOUR_GATEWAY_SECRET>A long random string your gateway signs conversation handles with
<YOUR_SITE_ORIGIN>The origin your chat page is served from, such as https://bayviewtaxi.example.com
<YOUR_PUBLIC_URL>The public HTTPS URL of your application, such as https://bayviewtaxi.example.com
<YOUR_CALLER_ID>A voice-capable number in your Space that the call comes from
<YOUR_DESTINATION>The rider’s phone number, in E.164 format
<YOUR_TOOL_WEBHOOK_URL>The HTTPS URL that receives your agent’s tool calls
<YOUR_POST_PROMPT_URL>The HTTPS URL that receives the conversation summary
2

Save an agent as a hosted SWML script

Save this document as a SWML Script resource in your Space’s Dashboard, or with the REST API. Its request URL, ending in /relay-bins/<YOUR_AGENT_ID>, is the address AI chat fetches your agent from. The same resource also answers voice calls.

version: 1.0.0
sections:
main:
- answer: {}
- ai:
prompt:
text: You are Ada, the dispatcher for Bayview Taxi. Answer questions about booking a ride. Keep replies to one or two sentences.

The REST response carries the request URL in swml_script.request_url.

3

Create the conversation

create_conversation opens a conversation with your agent under an ID you choose, and returns the agent’s opening line.

curl -X POST "https://<YOUR_SPACE>.signalwire.com/api/ai/chat" \
-u "<YOUR_PROJECT_ID>:<YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "create_conversation",
"params": {
"id": "chat-demo-1",
"config_url": "https://<YOUR_SPACE>.signalwire.com/relay-bins/<YOUR_AGENT_ID>"
}
}'
4

Send a message

Every turn after that is a chat request with the same conversation ID. The agent’s reply comes back in the same response.

curl -X POST "https://<YOUR_SPACE>.signalwire.com/api/ai/chat" \
-u "<YOUR_PROJECT_ID>:<YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-2",
"method": "chat",
"params": {
"id": "chat-demo-1",
"message": "I need a ride to the airport."
}
}'
5

Read the reply

A working turn returns the agent’s reply in result.response, usually within a few seconds. Check the HTTP status first. An HTTP 401 Unauthorized means the credentials are wrong or the token lacks the Chat permission, and an HTTP 400 means the request body wasn’t valid JSON.

Errors from AI chat itself arrive with HTTP 200 and an error object in place of result:

  • -32001, Conversation not found: the ID doesn’t match a live conversation. Check its spelling, or create the conversation again if it ended or sat idle past its timeout.
  • -32002, Configuration error: SignalWire couldn’t fetch your agent from config_url. Check the agent ID at the end of the URL.

The AI chat REST API reference lists the other error codes.

How an AI chat conversation works

Your agent is the SWML document at config_url. A conversation is a series of turns under an ID you choose, and IDs are scoped to your project. A turn is one user message and the agent’s reply, including any tool calls the agent makes along the way.

Your server sends create_conversation with an id and config_url. SignalWire fetches your agent's SWML document from config_url and returns the agent's greeting as initial_message. On each turn, your server sends chat with the id and message; if the agent calls a tool, SignalWire posts the tool call to your web_hook_url and your server returns the result, then SignalWire returns the agent's reply as response. Your server sends end_conversation, SignalWire returns status ended, then posts the summary to your post_prompt_url.

A conversation ID keeps only letters, digits, _, -, ., and :. Any other character is dropped without an error, so rider~2 is stored as rider2; use . when you compose IDs. A conversation ends when you end or delete it, or after it sits idle past its conversation_timeout, 3600 seconds unless you set one on the request that creates it. An ended conversation can’t be reopened: a later chat that passes config_url under the same ID starts a new conversation with no memory of the old one.

Hold an AI chat conversation

The AI chat REST API reference documents every method, parameter, and error code.

Hold an AI chat conversation via the Server SDKs

AIChatClient sends the same methods from the Server SDKs and turns errors into exceptions you can catch, such as ConversationNotFoundError when the conversation has ended or never existed. They all subclass AIChatError, as listed in the AIChatClient reference.

Python — AIChatClient
# Install: python -m pip install signalwire-sdk==3.4.1
# Save as chat_client.py and run: python chat_client.py
import asyncio
from signalwire.ai_chat import AIChatClient
CONFIG_URL = "https://<YOUR_SPACE>.signalwire.com/relay-bins/<YOUR_AGENT_ID>"
async def main():
async with AIChatClient(
project="<YOUR_PROJECT_ID>", token="<YOUR_API_TOKEN>", space="<YOUR_SPACE>"
) as client:
info = await client.create_conversation("chat-demo-2", config_url=CONFIG_URL)
print("Ada:", info.initial_message)
reply = await client.chat("chat-demo-2", "I need a ride to the airport.")
print("Ada:", reply.text)
log = await client.log("chat-demo-2")
for entry in log.messages:
if entry.get("role") in ("user", "assistant"):
print(f"{entry['role']}: {entry['content']}")
await client.end("chat-demo-2")
asyncio.run(main())

Hold an AI chat conversation via REST

Read the conversation back with chat_log, then end it with end_conversation. Ending starts post-processing, such as the summary sent to your post_prompt_url.

cURL — AI Chat API
curl -X POST "https://<YOUR_SPACE>.signalwire.com/api/ai/chat" \
-u "<YOUR_PROJECT_ID>:<YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-3",
"method": "chat_log",
"params": { "id": "chat-demo-1" }
}'
curl -X POST "https://<YOUR_SPACE>.signalwire.com/api/ai/chat" \
-u "<YOUR_PROJECT_ID>:<YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-4",
"method": "end_conversation",
"params": { "id": "chat-demo-1" }
}'

chat_log returns every entry in result.chat_log. end_conversation returns the conversation’s id with status: "ended", or status: "not_found" when no conversation has that ID.

Hold an AI chat conversation from a web page

ChatGateway runs inside your web application and forwards a page’s messages to AI chat. Your server holds the API token and the config_url. The page holds only the gateway’s URL and a publishable key, which can reach this one agent and nothing else, because the gateway injects config_url itself and never accepts it from the page.

publishable key project + API token the agent's reply JSON response Web pagegateway URL + publishable key Your applicationChatGateway SignalWireAI chat

The server below serves the chat page and mounts the gateway at /chat/. Localhost is always allowed, so it runs locally unconfigured; list your production origin in allowed_origins.

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as chat_gateway.py next to index.html and run: python chat_gateway.py
# Then open http://localhost:3000
import uvicorn
from fastapi import FastAPI
from fastapi.responses import FileResponse
from signalwire.ai_chat import AIChatClient, ChatGateway
gateway = ChatGateway(
config_url="https://<YOUR_SPACE>.signalwire.com/relay-bins/<YOUR_AGENT_ID>",
key="<YOUR_GATEWAY_KEY>",
secret="<YOUR_GATEWAY_SECRET>",
allowed_origins=["<YOUR_SITE_ORIGIN>"],
client=AIChatClient(
project="<YOUR_PROJECT_ID>", token="<YOUR_API_TOKEN>", space="<YOUR_SPACE>"
),
)
app = FastAPI()
app.include_router(gateway.router(), prefix="/chat")
@app.get("/")
async def page():
return FileResponse("index.html")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=3000)

The gateway takes four methods on one endpoint, each a POST with the key as a bearer token. The first start or chat returns a signed conversation handle in the X-Chat-Handle response header, and every later request sends it back as handle. See the gateway router reference.

MethodBodyReturns
start{"method": "start"}greeting, status, and timeout, plus the X-Chat-Handle header
chat{"method": "chat", "handle", "message"}The AI chat JSON-RPC response
log{"method": "log", "handle"}messages, reduced to user and assistant entries, with timeout and last_activity
end{"method": "end", "handle"}{"status": "ended"}

A key copied from your page lets someone talk to your agent, and every turn is billed to your project. The origin allowlist stops the key working in someone else’s page, but not in a script, which can leave out the Origin header. The gateway caps new conversations at 60 a minute and turns at 200 per conversation by default; lower them with max_new_conversations and max_turns.

Hold an AI chat conversation over SMS text messages

Answer text messages to your number with the agent. When a message arrives, SignalWire posts the inbound message webhook to your server, which sends the text to AI chat and returns a messaging SWML document whose reply carries the agent’s answer.

Python — AIChatClient
# Install: python -m pip install signalwire-sdk==3.4.1
# Save as chat_sms.py and run: python chat_sms.py
import uvicorn
from fastapi import FastAPI, Request
from signalwire.ai_chat import AIChatClient
CONFIG_URL = "https://<YOUR_SPACE>.signalwire.com/relay-bins/<YOUR_AGENT_ID>"
client = AIChatClient(project="<YOUR_PROJECT_ID>", token="<YOUR_API_TOKEN>", space="<YOUR_SPACE>")
app = FastAPI()
@app.post("/sms")
async def sms(request: Request):
message = (await request.json())["message"]
if not message.get("body"):
return {"version": "1.0.0", "sections": {"main": [{"reply": "Please send your question as text."}]}}
conversation_id = "sms." + message["from"].lstrip("+")
reply = await client.chat(conversation_id, message["body"], config_url=CONFIG_URL)
return {"version": "1.0.0", "sections": {"main": [{"reply": reply.text}]}}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=3000)

Then route your number’s messages to this server. Create a SWML Webhook resource whose URL is your server’s public /sms URL, then assign it to your number under Inbound Message Settings, as the webhooks page shows. Over the REST API, create the SWML webhook with used_for set to messaging, then assign it to the number with handler set to messaging.

Passing config_url on chat creates the conversation on the first message from a sender and continues it after that, keyed on the sender’s number. Anyone who can reach /sms can run turns billed to your project, so in production verify the webhook signature on each request before acting on it.

Move an AI chat conversation onto a phone call and back

A rider chatting with Ada can take the conversation onto a phone call and back to text without repeating themselves. The conversation stays open during the call. Your server reads it, places a call whose agent starts with the chat so far, and when the call ends, writes what was said on the call back into the conversation as a system message. Only two APIs are involved: AI chat, and the Calling API through the REST client’s calling.dial.

Python — AIChatClient and REST client
# Install: python -m pip install signalwire-sdk==3.4.1
# Save as chat_call.py and run: python chat_call.py
# SignalWire must reach /post_prompt at <YOUR_PUBLIC_URL>.
import asyncio
import uvicorn
from fastapi import FastAPI, Request
from signalwire.ai_chat import AIChatClient
from signalwire.rest import RestClient
PUBLIC_URL = "<YOUR_PUBLIC_URL>"
PROMPT = ("You are Ada, the dispatcher for Bayview Taxi. Answer questions about booking a ride. "
"Keep replies to one or two sentences.")
chat = AIChatClient(project="<YOUR_PROJECT_ID>", token="<YOUR_API_TOKEN>", space="<YOUR_SPACE>")
rest = RestClient(project="<YOUR_PROJECT_ID>", token="<YOUR_API_TOKEN>", host="<YOUR_SPACE>.signalwire.com")
app = FastAPI()
def dialogue(messages):
return "\n".join(f"{m['role']}: {m['content']}" for m in messages
if m.get("role") in ("user", "assistant") and m.get("content"))
@app.post("/call")
async def call(request: Request):
body = await request.json()
so_far = dialogue((await chat.log(body["conversation_id"])).messages)
swml = {"version": "1.0.0", "sections": {"main": [{"answer": {}}, {"ai": {
"prompt": {"text": f"{PROMPT}\n\nThe rider is moving from text chat to this call. So far:\n{so_far}"},
"post_prompt": {"text": "Summarize the call in one sentence."},
"post_prompt_url": f"{PUBLIC_URL}/post_prompt?conversation_id={body['conversation_id']}",
}}]}}
placed = await asyncio.to_thread(rest.calling.dial, from_="<YOUR_CALLER_ID>", to=body["phone"], swml=swml)
return {"call_id": placed["id"]}
@app.post("/post_prompt")
async def post_prompt(request: Request):
call_log = (await request.json()).get("call_log", [])
reply = await chat.chat(request.query_params["conversation_id"], role="system",
message="The rider just spoke with you on a phone call:\n" + dialogue(call_log))
print("Ada:", reply.text)
return {}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=3000)

Create a conversation as in the first run, under the ID chat-demo-3, and send it a message or two. Then ask your server to move it onto the rider’s phone:

cURL
curl -X POST "<YOUR_PUBLIC_URL>/call" \
-H "Content-Type: application/json" \
-d '{"conversation_id": "chat-demo-3", "phone": "<YOUR_DESTINATION>"}'

The phone rings, and Ada already knows what was typed. Hang up, and a few seconds later the server prints Ada’s reply to the call; the next chat on chat-demo-3 picks up from there. If the chat doesn’t know about the call, SignalWire couldn’t reach /post_prompt. If the conversation sits idle past its conversation_timeout during the call, it ends, and the call has nowhere to write back to.

/call places calls on your bill, so only your own backend should reach it. /post_prompt writes into whichever conversation its query string names, so anyone who reaches it can slip a system message into a conversation whose ID they know or guess. In production, verify the webhook signature on each request before acting on it.

A web page can move its conversation into a call in the browser instead, through HandoffRouter mounted beside the gateway. The page places the call with the Browser SDK’s dial(), passing the chat handle and a nonce in userVariables. Your application registers that nonce with register when SignalWire fetches the call’s configuration, and supplies the capture_leg and end_call callbacks that record each leg and hang up the call. This path ends the text leg when the call starts, so returning to text continues under a new ID, <id>.1.

AI chat methods: create, chat, read, summarize, end, and delete

MethodWhat it doesWorth knowing
create_conversationCreates a conversationReturns initial_message when the agent speaks first. On an ID that is already live it returns status: "exists" and no greeting; pass reinit: true to start it over
chatSends a message and returns the replyWith config_url, it creates the conversation if needed, but you never get initial_message
chat_logReads the conversation backChanges nothing
summarizeSummarizes the conversation on demandTakes an optional summary_prompt
end_conversationEnds the conversation and starts post-processingTriggers the summary to your post_prompt_url
deleteRemoves the conversation and its messagesNothing is post-processed and no webhook fires

Messages take a role of user or system. A system message steers the agent without appearing as something the user said.

Receive tool calls and summaries from an AI chat conversation

A chat conversation sends the same webhooks as a voice AI session. Tool calls reach the web_hook_url on your SWAIG functions, and the summary reaches your post_prompt_url a few seconds after the conversation ends. Tool calling covers the round trip, and webhooks covers handling them in general.

JSON
{
"version": "1.0.0",
"sections": {
"main": [
{ "answer": {} },
{
"ai": {
"prompt": {
"text": "You are Ada, the dispatcher for Bayview Taxi. When the rider gives a pickup address and time, call book_ride. Keep replies to one or two sentences."
},
"post_prompt": { "text": "Summarize the booking in one sentence." },
"post_prompt_url": "<YOUR_POST_PROMPT_URL>",
"SWAIG": {
"functions": [
{
"function": "book_ride",
"description": "Book a taxi ride",
"parameters": {
"type": "object",
"properties": {
"pickup": { "type": "string", "description": "Pickup address" },
"time": { "type": "string", "description": "Pickup time" }
}
},
"web_hook_url": "<YOUR_TOOL_WEBHOOK_URL>"
}
]
}
}
}
]
}
}

Both webhooks identify a chat, so one handler can serve voice and text. SignalWire also appends conversation_id and conversation_type to the tool webhook’s query string.

FieldTool webhookPost-prompt callback
conversation_typechatchat
conversation_idYour conversation IDYour conversation ID
call_idYour conversation IDA separate UUID

The summary arrives in post_prompt_data.raw.

Transcripts contain your system prompt

chat_log and the post-prompt call_log include your prompt, system messages, and tool entries alongside the dialogue. Filter to user and assistant before showing either to anyone. The gateway’s log already does.

Next steps