On a web call (the widget, the hosted page, your app) the caller sees a chat beside the voice, so the agent can do more than speak. Phone callers get none of this.
Turn on Agent → Tools → Send to the caller (send_to_caller in the agent API) and the agent can put up to five cards at a time on the caller's screen: a title, a line under it, a description, a picture, a link, and up to two reply buttons each; it can also offer up to three quick replies under a question. Links are never read out.
The agent may only send links and pictures it was given in that call: its instructions and greeting, the knowledge base (a document read from a web page carries its address), your functions' answers and the ready-made cards. A link must also be on one of allowed_hosts (a ready-made card's own site always is). Links the agent makes up, or that the caller reads out, are not sent. Links on those websites in the agent's words are clickable in the chat.
"send_to_caller": {
"enabled": true,
"allowed_hosts": ["shop.example.com", "*.cdn.example.com"],
"presets": [
{ "name": "hours", "when": "asked when we open", "title": "Opening hours",
"description": "Mon–Sat 9:00–18:00", "url": "https://shop.example.com/stores", "image_url": "" }
]
}
Each send is sent to you as the call.message_sent webhook (channel, items with title and url, quickReplies, blocked). A call sends at most 10 times.
During the call the caller can also type (an email address, an order number) or tap a reply button; the agent answers it like speech, stopping first if it was talking. Up to 500 characters a message, one a second, 100 a call. In the call record these turns are marked as typed or tapped.
Visitors can also start by typing instead of talking: the widget and the hosted page offer both, or only one of them if you choose so in Agent → Deploy. A typed conversation has no voice and is billed per message. To chat with an agent from your own server, use the Chat API.
The widget gives your page an evasay object. Everything is optional, and the call frame only listens to your page when it is one of the widget's allowed websites (Agent → Deploy):
<script src="https://evasay.com/voice-widget.js" data-agent="AGENT_SLUG" async></script>
<script>
window.evasay = window.evasay || [];
evasay.push(function (agent) {
agent.setVariables({ plan: 'pro', page: location.pathname }); // for the next call
agent.update('Cart: 2 items, $120', { key: 'cart' }); // during it
agent.registerTool('add_to_cart', async function (args) {
// your code, in the visitor's browser
return { ok: true, checkout: 'https://shop.example.com/checkout' };
});
agent.on('cards', function (cards) {}); // also 'live', 'ended'
});
</script>
setVariables values fill {{name}} in the instructions and greeting. They come from the visitor's browser, so the agent is told they are not verified, and they never reach your functions or the start lookup.update tells the agent what happens on the page without making it answer; a newer update with the same key replaces the older one."kind": "page", no url) to the agent, and register a function of the same name on your page. The agent may call it only on calls from a page that registered it; it waits up to timeout_sec (1–10, default 5) for the answer unless wait_for_result is false. It runs signed in as the visitor, like a click of theirs: ask for confirmation in your own page before anything that costs money or cannot be undone. Links in its answer may be sent as cards.Set Who can start a call to Only with a token, and have your server get a one-time token for the signed-in user:
curl https://evasay.com/api/v1/voice/web-calls \
-X POST \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{ "agent_id": "AGENT_ID", "user_id": "u_42", "variables": { "name": "Ann" }, "expires_in": 600 }'
The answer has token (one call, within expires_in seconds, 30–3600, default 600), agent (the slug) and web_call_url. Give the token to the page with data-token-url="/your/endpoint" on the widget's script (it POSTs there with the visitor's cookies and reads { "token": "…" }) or agent.setToken(async () => token). The token's variables are verified: the instructions, the start lookup and your functions' templates may use them, as may user_id, which the call keeps (userId in the call, user_id to filter calls). Keep your API key on your server.
In your own app, POST { "token": "…", "pageTools": ["add_to_cart"] } to https://evasay.com + web_call_url. The answer is the call's connection (a WebRTC offer address, or a WebSocket address, with its call token), for an RTVI client such as Pipecat's client SDK; for iOS, Android and React Native see Mobile apps. Cards, page functions and notices arrive as RTVI server-messages (evasay.cards, evasay.tool, evasay.notice); send typed words with send-text and page reports and answers as client-messages evasay.context and evasay.tool_result. Or carry the audio yourself with Audio over a WebSocket, passing user_id and page_tools.