Skip to content

Telephony Quickstart

This content is not available in your language yet.

This is the full flow: your server asks Wetel to start a call, your page joins the call with the LiveKit client, and the agent answers. Read the overview for what a call is.

  • A named API key for your workspace (the same kind sdkStart uses; see Authentication).
  • An agent in that workspace with a published workflow. Make it voice-safe before you test.
  • Voice enabled on your environment, and a test session arranged with Wetel.
  • A server that can make HTTPS requests, and a page served over HTTPS (browsers only give a page the microphone on a secure origin, or localhost).

Call the sdkStartVoiceCall mutation with your API key. Never call it from a browser: the key must not reach one.

// server.mjs — Node 20+
const API_URL = process.env.WETEL_API_URL; // the GraphQL endpoint you were given
const API_KEY = process.env.WETEL_API_KEY; // server-side only
const query = `
mutation StartVoiceCall($input: SdkStartVoiceCallInput!) {
sdkStartVoiceCall(input: $input) {
sessionId
agentId
livekitUrl
livekitToken
roomName
expiresAt
}
}`;
export async function startVoiceCall(agentId, callerId) {
const res = await fetch(API_URL, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": API_KEY,
"x-huat-platform": "customer",
},
body: JSON.stringify({
query,
variables: {
input: {
agentId,
clientId: callerId, // your own id for this caller or call
context: { source: "web-call" }, // optional, flat object
},
},
}),
});
const json = await res.json();
if (json.errors) throw new Error(json.errors[0].message);
const { livekitUrl, livekitToken, sessionId } = json.data.sdkStartVoiceCall;
return { livekitUrl, livekitToken, sessionId };
}

Expose it to your page through your own endpoint, for example POST /my-app/start-call, which checks that the caller is allowed to call and returns only livekitUrl and livekitToken. The token is valid to join for about two minutes, so mint it when the caller presses the button, not earlier.

This minimal page uses the LiveKit JavaScript client (livekit-client). It connects, turns the microphone on, plays the agent’s audio, shows the agent’s state and prints the live transcript.

<!doctype html>
<meta charset="utf-8" />
<title>Voice call test</title>
<button id="call">Call the agent</button>
<button id="hangup" disabled>Hang up</button>
<p>Agent state: <b id="state">-</b></p>
<pre id="transcript"></pre>
<div id="audio"></div>
<script src="https://cdn.jsdelivr.net/npm/livekit-client@2/dist/livekit-client.umd.min.js"></script>
<script>
const { Room, RoomEvent, Track } = LivekitClient;
let room;
document.getElementById("call").onclick = async () => {
// Your own endpoint from step 1. It returns { livekitUrl, livekitToken }.
const r = await fetch("/my-app/start-call", { method: "POST" });
const { livekitUrl, livekitToken } = await r.json();
room = new Room();
// Play the agent's audio.
room.on(RoomEvent.TrackSubscribed, track => {
if (track.kind === Track.Kind.Audio) {
document.getElementById("audio").appendChild(track.attach());
}
});
// Agent state: listening, thinking or speaking.
room.on(RoomEvent.ParticipantAttributesChanged, (changed, participant) => {
if (changed["lk.agent.state"]) {
document.getElementById("state").textContent =
participant.attributes["lk.agent.state"];
}
});
// Live transcript (the caller's speech and the agent's replies).
room.registerTextStreamHandler("lk.transcription", async (reader, info) => {
const text = await reader.readAll();
const final =
reader.info.attributes?.["lk.transcription_final"] === "true";
if (final) {
document.getElementById("transcript").textContent +=
`${info.identity}: ${text}\n`;
}
});
room.on(RoomEvent.Disconnected, () => {
document.getElementById("hangup").disabled = true;
});
await room.connect(livekitUrl, livekitToken);
await room.localParticipant.setMicrophoneEnabled(true);
document.getElementById("hangup").disabled = false;
};
document.getElementById("hangup").onclick = () => room && room.disconnect();
</script>

Notes on the code:

  • The browser asks for microphone permission at setMicrophoneEnabled(true). Handle a refusal; the call is useless without it.
  • lk.agent.state is a LiveKit participant attribute that the agent participant sets to listening, thinking or speaking. Use it for a “the agent is thinking” indicator.
  • Transcription arrives on the text stream topic lk.transcription, and each stream carries the attributes lk.segment_id (which line it belongs to) and lk.transcription_final ("true" once the line will not change). The example above prints only lines marked final. Treat the exact client API as that of the livekit-client version you install, and pin it.
  • Pin the client script to an exact version in production rather than @2.
  1. The agent joins the room as soon as the caller joins and speaks first: a greeting that says it is an AI and offers a person.
  2. Talk. After you stop, the agent answers in about 3 to 5 s today.
  3. Talk over the agent. It stops speaking and answers what you said instead.
  4. Hang up. The session stays in the workspace like a chat session; read it with the sessions API.
SymptomLikely cause
Missing X-Api-Key header / Invalid API keyHeader missing or key wrong or revoked (UNAUTHENTICATED).
Voice calls are not enabled on this environmentVoice is not switched on for this environment (FORBIDDEN). Ask for a test session.
Platform is not specified!The x-huat-platform: customer header is missing.
Agent not foundagentId is not an agent of the key’s workspace (NOT_FOUND).
Joins, but nobody speaksNo voice worker is running for your session. Check the time window with your Wetel contact.
Join refusedThe token is older than about two minutes. Start a new call.
“Too many requests” style errorRate limit; wait a short while and retry. See the reference.

When you report a problem, send the sessionId, the time with seconds and timezone, and what you saw. Never send the livekitToken or your API key.