Guide
SwissAI gives you voice agents that answer phone calls. You create them in the dashboard with a prompt, a language and optional tools; each agent gets a SIP URI. Route calls to that URI and the agent picks up, talks to the caller and calls your HTTP endpoints when it needs data or wants to act.
Everything is configured from the dashboard: agents, tools, your customers and billing.
Agents
You create and edit agents in the dashboard: name, language, scope (the prompt), an optional knowledge base, the models and the tools. Each agent gets an id and a SIP URI, and can be assigned to one of your customers.
Example of a scope
Write the scope as you would brief a new colleague: who the agent is, what it can do, what it must never do and how the call ends.
You are Anna, the assistant of Dental Practice Keller in Zurich. You answer the phone when the reception is busy or closed. What you do: - book, move or cancel appointments, using the tools; - answer questions about opening hours, address and prices (see the knowledge base). Rules: - speak briefly and politely, one question at a time; - before booking, repeat day, time and name and wait for a clear yes; - never give medical advice: for pain or emergencies offer the first free appointment and give the emergency number; - if you cannot help, take name and phone number and say the practice will call back. Close the call by repeating what was agreed.
Tools
Tool definition
A tool is an HTTP endpoint on your side described for the model with a JSON schema. The model decides when to call it and with which arguments; the platform performs the HTTP request.
{
"name": "book_appointment",
"description": "Books an appointment. Call it only after the caller confirmed day, time and name.",
"method": "POST",
"url": "https://api.example.ch/clinics/{{clinic}}/appointments",
"headers": { "Authorization": "Bearer sk_…" },
"content_type": "application/json",
"body": "{ \"start\": \"{{start}}\", \"name\": \"{{name}}\", \"phone\": \"{{phone}}\" }",
"schema": {
"type": "object",
"properties": {
"clinic": { "type": "string", "description": "Clinic code: always 'zurich'" },
"start": { "type": "string", "description": "Start, yyyy-MM-dd HH:mm, e.g. 2026-10-06 10:30" },
"name": { "type": "string", "description": "Caller's first and last name" },
"phone": { "type": "string", "description": "Caller's phone number, digits only" }
},
"required": ["clinic", "start", "name", "phone"]
}
}
| Field | For | Description |
|---|---|---|
| name | model | Function name, letters and underscores. |
| description | model | What it does and when to call it. Write it for the model. |
| schema | model | JSON schema of the arguments. |
| response_schema | model | Optional. Shape of your response, so the model can read it. |
| url | runtime | Your endpoint, HTTPS. |
| method | runtime | GET (arguments in the query string) or POST (arguments as JSON body). |
| content_type | runtime | Usually application/json. |
| headers | runtime | Optional. Headers sent on every call, e.g. Authorization; values may contain {{parameters}}. |
| body | runtime | Optional. Body template with {{parameters}}. Empty: the parameters not used in url or headers are sent as query string (GET, DELETE) or JSON body. |
Parameters and placeholders
Write {{name}} in url, header values or body. Every placeholder must be declared in schema.properties with a description: the model fills it from the conversation and the runtime substitutes it (URL-encoded in the url, escaped in a JSON body). The dashboard builds the schema from the placeholders and checks the tool with Claude before saving.
How the agent calls you
Every request carries the header token with the agent's tool_token. Respond with 200 and a JSON body:
- Success: { "success": true, "message": "…" } plus any data. The message is meant to be relayed to the caller.
- Business error: { "success": false, "error": "…" }. Write the error as a sentence for the model: what to ask or propose instead.
- 401 / 500: the agent apologises and offers a callback. Keep responses under a few seconds: the caller is waiting on the line.
Your endpoint: examples
This is the request the platform sends for the tool above, and a minimal endpoint that answers it. Check the token header first: it is the tool token you set on the agent.
POST /clinics/zurich/appointments HTTP/1.1 Host: api.example.ch Authorization: Bearer sk_… token: 5f1c… # the agent's tool token Content-Type: application/json { "start": "2026-10-06 10:30", "name": "Anna Keller", "phone": "41791234567" }
app.post('/clinics/:clinic/appointments', express.json(), async (req, res) => { // 1. only your agent may call if (req.get('token') !== process.env.SWISSAI_TOOL_TOKEN) return res.status(401).end(); const { start, name, phone } = req.body; // 2. business error: a sentence the agent can act on if (!(await isFree(req.params.clinic, start))) return res.json({ success: false, error: "That time is taken. Offer 11:00 or 14:30 on the same day." }); // 3. success: the message is what the agent tells the caller await book(req.params.clinic, start, name, phone); res.json({ success: true, message: "Appointment booked for " + start + '.' }); });
<?php // appointments.php header('Content-Type: application/json'); if (($_SERVER['HTTP_TOKEN'] ?? '') !== getenv('SWISSAI_TOOL_TOKEN')) { http_response_code(401); exit; } $in = json_decode(file_get_contents('php://input'), true); if (!is_free($in['start'])) { echo json_encode(['success' => false, 'error' => "That time is taken. Offer 11:00 or 14:30 on the same day."]); exit; } book($in['start'], $in['name'], $in['phone']); echo json_encode(['success' => true, 'message' => "Appointment booked for " . $in['start'] . '.']);
The Tools section
Tools live in one list, the Tools section of the dashboard, and agents use them by reference: edit a tool once and every agent that uses it is updated. A tool assigned to a customer can be used only by that customer's agents; a tool with no customer can be used by all your agents. In the agent editor you pick the tools from the list, or create a new one that is added to it.
Sandbox: ready-made tools and example code
The sandbox is a live set of example endpoints with sample data, separate for each account: calendar, restaurant tables, stock, callbacks, contacts, orders. Its 22 tools are already in the Tools section: add them to an agent and try it in minutes, with nothing to host. Each tool has a guide with its parameters, a worked request and response and the code of the endpoint that answers it, which you can download and use as the starting point for your own.
Every account has its own test environment with the same starting data: three bookable services with one appointment already taken, eight restaurant tables with one reservation, six products, two contacts and four orders. It is created automatically the first time you add a sandbox tool to an agent, or from the Tools section, where you can also bring it back to the starting data. An environment not used for 30 days is deleted and can be created again.
Adding a sandbox tool to an agent is all it takes: the dashboard fills in the address with the key of your environment. To call the endpoints yourself, send the key in the header Authorization: Bearer ws_… (in the dashboard, the guide of each tool shows the command with your key). Calls are GET, with the arguments in the query string, or POST, with the arguments as JSON.
Every answer has status 200 and a JSON body: success true with a message to tell the caller plus the data, or success false with an error written as a sentence for the agent, for example which times are free instead. Days are yyyy-MM-dd, times HH:mm, phone numbers digits only. Below, each area with what it is, where it is used and all its calls, each with an example request and the real answer.
Customers
A customer is a client of yours. Assign agents to a customer in the agent editor; the customer receives an invite by email and can sign in (password, Google or Apple) to see their agents, calls and monthly usage priced at the rate you set. The customer pays you: SwissAI only shows the figures.
Invite, edit, remove
Creating a customer in the dashboard sends the invite. Removing it unassigns the agents and revokes the customer's access.
Assign an agent
In the agent editor choose the customer who sees its calls; Personal use keeps the agent for you only.
Telephony
An agent answers the calls that reach its SIP URI on our switchboard. There are three ways to bring a call there, all set up from the Telephony menu of the dashboard: an external PBX enabled by IP address, a phone number, and a WebRTC client on a website. From the dashboard you can also call any agent from the browser to test it; test calls count as minutes like any other call.
SIP URI and external PBXs
Every agent has a SIP URI, shown in the agents list, like sip:ag_7f3a2b91@pbx.swissai.dev. Your PBX sends the call to that address: the part before the @ chooses the agent.
Calls are accepted only from PBXs you have declared. In Telephony › External PBXs add the PBX with a name, the customer it belongs to (or Personal use) and the IP address its calls come from. There is no username or password: the PBX is recognised by its IP address and can call all the agents of that customer.
; extensions.conf: extension 200 rings the agent
exten => 200,1,Dial(SIP/ag_7f3a2b91@pbx.swissai.dev,60)
same => n,Hangup()
; pjsip.conf: the SwissAI switchboard as an outbound endpoint, no authentication [swissai] type=endpoint context=from-swissai disallow=all allow=alaw,ulaw aors=swissai [swissai] type=aor contact=sip:pbx.swissai.dev:5060 ; extensions.conf: extension 200 rings the agent exten => 200,1,Dial(PJSIP/ag_7f3a2b91@swissai,60) same => n,Hangup()
<!-- dialplan: extension 200 rings the agent -->
<extension name="swissai_agent">
<condition field="destination_number" expression="^200$">
<action application="bridge" data="sofia/external/ag_7f3a2b91@pbx.swissai.dev"/>
</condition>
</extension>
# kamailio.cfg / opensips.cfg, in the request route: calls to 200 go to the agent if ($rU == "200") { $ru = "sip:ag_7f3a2b91@pbx.swissai.dev:5060"; t_relay(); exit; }
Calls from an address you have not declared are rejected, and an address that keeps trying is blocked for a while. If your PBX changes IP address, update it in the dashboard before sending calls.
Numbers
A number brings ordinary phone calls to an agent. In Telephony › Numbers every number shows the SIP account used to register it (username, password, host, port) and the agent that answers.
- A number you already have: with New number enter the number and the SIP account your operator gave you: host, port, username and password. We register with your operator and the calls to that number reach the agent you choose.
- A new number: with Request number choose who the number is for (a customer or Personal use), the prefix you want, and attach an identity document, plus the commercial register extract when the number is for a company. The prefix must match the applicant's place of residence. The request stays in the list as awaiting the provider; once the number is assigned it becomes active and you only choose the agent.
WebRTC clients (click to call)
A WebRTC client is a call button on a website: the visitor talks to the agent from the browser, without a phone. Create it in Telephony › WebRTC clients: choose the agent and list the domains where the button is used (add localhost to test on your machine). You get the client name (wc_…) and a secret key (wk_…), shown only once.
The secret key must never reach the browser. The page asks your backend, your backend asks us for a temporary password with the key, and hands it to the page. The temporary password is valid for 60 seconds and for one call.
curl -X POST https://developers.swissai.dev/api/webrtc/token \ -H "X-Api-Key: wk_your_secret_key" \ -H "Content-Type: application/json" \ -d '{"client": "wc_3f9a1c2b7d4e", "identity": "visitor-1842"}'
// 200 { "wss": "wss://pbx.swissai.dev:8089", "domain": "pbx.swissai.dev", "user": "w4c1f09ab37de5521", "pass": "9b2e61f0c4a87d35e1f2a6b90c7d4e88", "target": "sip:ag_7f3a2b91@pbx.swissai.dev", "expires_in": 60 }
| Field | Description |
|---|---|
| client | Required. The client name shown in the dashboard. |
| identity | Optional. Your reference for the visitor, for example a user id. |
| 401 | Unknown client or wrong key. |
| 503 | Browser calls are not available for this client, for example because its agent was deleted. |
// your backend: the secret key stays here app.post('/call-token', async (req, res) => { const r = await fetch('https://developers.swissai.dev/api/webrtc/token', { method: 'POST', headers: { 'X-Api-Key': process.env.SWISSAI_CLIENT_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ client: 'wc_3f9a1c2b7d4e' }) }); res.status(r.status).json(await r.json()); });
<?php // call-token.php: the secret key stays on your backend $ch = curl_init('https://developers.swissai.dev/api/webrtc/token'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-Api-Key: ' . getenv('SWISSAI_CLIENT_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['client' => 'wc_3f9a1c2b7d4e']), ]); $body = curl_exec($ch); http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE)); header('Content-Type: application/json'); echo $body;
Load our script and pass it the response as it is. onState receives connecting, calling, connected and idle; onError receives the reason when the call cannot start.
<button id="call">Call us</button> <button id="hangup" hidden>Hang up</button> <p id="status"></p> <script src="https://developers.swissai.dev/js/call.js"></script> <script> const call = document.getElementById('call'), hangup = document.getElementById('hangup'), status = document.getElementById('status'); call.onclick = async () => { const params = await (await fetch('/call-token', { method: 'POST' })).json(); // your backend, step 1 SwissCall.start(params, { onState: s => { status.textContent = s; call.hidden = s !== 'idle'; hangup.hidden = s === 'idle'; }, onError: m => { status.textContent = m; } }); }; hangup.onclick = () => SwissCall.hangup(); </script>
GetCalls: calls and what they cost your customer
With the name and the secret key of a WebRTC client your backend can read the calls of the customer that client belongs to (all the agents of that customer) and what each call costs the customer at the price list you set in Customers.
curl "https://developers.swissai.dev/api/calls?client=wc_3f9a1c2b7d4e&month=2026-10" \ -H "X-Api-Key: wk_your_secret_key"
// 200 { "month": "2026-10", "customer": { "customer_id": 7, "name": "Dental Practice Keller" }, "currency": "CHF", "rate": 0.25, "free_minutes": 100, "calls": [ { "call_ref": "c-91f3a2", "agent_id": "ag_7f3a2b91", "agent_name": "Anna", "source": "webrtc", "from": "", "started": "2026-10-06 10:41:03", "seconds": 84, "outcome": "appointment", "tool_calls": 2, "cost": 0.35 } ], "total": { "minutes": 312.4, "calls": 148, "billable_minutes": 213, "amount": 53.25 } }
| Field | Description |
|---|---|
| client | Required. The client name shown in the dashboard. |
| month | Optional. yyyy-MM; the current month when missing. Up to 500 calls, newest first. |
| cost | The duration of the call at the customer's price per minute, before the included minutes. |
| total | The month as the customer sees it in their portal: included minutes deducted, seconds rounded up to the minute on the monthly total. |
| customer | null when the client's agent is for personal use: the calls are those of your agents without a customer, at the platform price. |
You can try both calls, with the code to copy, from the Test area of the dashboard.
Chat: text messages to the agent
The same agent that answers the phone can answer in writing: on your site, in your app or on a messaging channel you manage. Your backend sends what the user wrote and gets the reply as text; while answering, the agent can call its tools as it does on the phone.
The call is made with the name and the secret key of a WebRTC client, which choose the agent, and must come from your backend: the key must never reach the browser. It keeps no state: send the last exchanges in history every time.
curl -X POST https://developers.swissai.dev/api/messages \ -H "X-Api-Key: wk_your_secret_key" \ -H "Content-Type: application/json" \ -d '{ "client": "wc_3f9a1c2b7d4e", "user": "visitor-1842", "name": "Laura Meier", "message": "Is Thursday at 10 still free?", "history": [ { "direction": "in", "text": "Hello, I would like to book a check-up." }, { "direction": "out", "text": "Of course. Which day would suit you?" } ] }'
// 200 { "reply": "Yes, Thursday at 10:00 is free. Shall I book it for you?" }
| Field | Description |
|---|---|
| client | Required. The client name shown in the dashboard. |
| message | Required. The text the user wrote, up to 4000 characters. |
| user | Optional. Your reference for who is writing, for example a user id or a phone number. |
| name | Optional. The name of who is writing, when you know it. |
| history | Optional. The last exchanges, oldest first, up to 20: direction is in for the user and out for the agent, text is the message. |
| reply | The agent's answer to show to the user; empty when the agent has nothing to say. |
| 401 | Unknown client or wrong key. |
| 402 | The trial or the plan is not active, or the wallet has no credit. |
| 502 | The agent could not answer: try again. |
You can chat with any agent from Telephony › Chat, and try this call with the code to copy from the Test area.
WhatsApp Business
An agent can answer on WhatsApp in two ways. Either you link the number from the dashboard and SwissAI receives the messages and replies, or you keep your own Meta app and webhook on your site and send each message to the agent with the chat call above.
A. Link the number from the dashboard
Telephony › WhatsApp › Link with WhatsApp. A Meta window opens: you sign in with the Facebook account of your business, choose the business portfolio (or create one) and the number; the link completes by itself. You choose the agent that answers and can change it later from the list.
Two ways to use the number: dedicated to the agent (the number is no longer used from the phone app), or keep WhatsApp Business on the phone, where the agent answers and you keep using the app. The second needs the updated WhatsApp Business app and a number in real use on it for at least a week, otherwise Meta rejects it.
Every incoming message goes to the agent together with the last 20 exchanges of that conversation, and the reply is sent back on WhatsApp; voice messages are transcribed. Each exchange appears in Logs. The agent answers only while the trial or a plan is active and the wallet has credit; messages do not consume credit. Meta bills the conversations at its own rates and needs a payment method on your business account before the agent can reply.
Unlink from the same page: the number is released from the Cloud API and the conversation history on the portal is deleted.
B. Your own Meta app and webhook
If you already run the WhatsApp Cloud API with your own Meta app, keep it. Your webhook receives the message, sends it to the agent with the chat call (user is the sender's number, name the profile name, history the last exchanges, which you keep yourself: the call has no state) and sends the reply with the Graph API. The agent is the one of the WebRTC client you name; its secret key stays on your server.
// your backend: Meta webhook → SwissAI agent → Graph API app.post('/webhook', express.json(), async (req, res) => { res.sendStatus(200); // answer Meta at once, then work for (const entry of req.body.entry || []) for (const change of entry.changes || []) { const v = change.value, m = (v.messages || [])[0]; if (!m || m.type !== 'text') continue; const name = ((v.contacts || [])[0]?.profile || {}).name || ''; const r = await fetch('https://developers.swissai.dev/api/messages', { method: 'POST', headers: { 'X-Api-Key': process.env.SWISSAI_CLIENT_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ client: 'wc_3f9a1c2b7d4e', user: '+' + m.from, name: name, message: m.text.body, history: lastExchanges(m.from) // the last exchanges you stored, oldest first }) }).then(r => r.json()); if (!r.reply) continue; // the reply goes back with the Graph API, from your number await fetch('https://graph.facebook.com/v25.0/' + v.metadata.phone_number_id + '/messages', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.WA_TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ messaging_product: 'whatsapp', to: m.from, type: 'text', text: { body: r.reply } }) }); } });
The webhook verification (GET with hub.verify_token and hub.challenge), the signature check of each event (X-Hub-Signature-256) and the subscription to the messages field are those of the WhatsApp Cloud API: see Meta's documentation. Store the last exchanges per sender so you can send them in history: the agent only knows what you send.
Both ways use the same agent, with its scope and tools: pick A if you do not want to run a webhook, B if WhatsApp is already part of your platform.
Billing
Every account starts with a 30-day free trial that includes 100 minutes; no card is needed. After the trial you choose a plan in Billing. The monthly fee is loaded into your wallet as plan credit, and each call is deducted from it per second at the per-minute rate of the plan. Plan credit resets at every renewal. When it runs out you can top up the wallet: top-up credit never expires and is used after the plan credit. Phone numbers issued on request are charged to the wallet every month. Agents answer only while the trial or a plan is active and the wallet has credit.