Build a softphone
Call and answer from a web page with the browser SDK, in a few lines.
Complete example
06line/example-softphone is a working softphone in Next.js built on this
page: clone it, put your API key in .env.local and run it.
@06line/sdk-browser does the plumbing of a call in a browser: the WebRTC session and its reconnections, the ephemeral keys,
the microphone, the choice of devices, the volumes. You write the interface. If you prefer to drive the calls yourself,
everything it does is described in Calls.
Install
npm install @06line/sdk-browserGive the browser an ephemeral key
The browser never sees your API key. Add a route to your server that asks 06 Line for an ephemeral key and returns it as is, after checking that your user may call:
export async function POST() {
const res = await fetch('https://api.06line.com/v1/ephemeral-keys', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SIXLINE_API_KEY}` },
});
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
}The SDK calls this route when it starts, then again shortly before each key expires (every 15 minutes).
Create the phone
import { createPhone } from '@06line/sdk-browser';
const phone = createPhone({
getEphemeralKey: () => fetch('/api/06line-key', { method: 'POST' }).then((r) => r.json()),
});
phone.on('state', (state) => (statusEl.textContent = state)); // connecting, connected, reconnecting, closedLet the user pick devices
const { inputs, outputs } = await phone.devices();
fillSelect(micSelect, inputs);
fillSelect(speakerSelect, outputs);
speakerSelect.hidden = !phone.canChooseOutput; // Safari: sound follows the system outputDevice names stay empty until the user has granted microphone access once; phone.testMicrophone() is a good moment
to ask, and lets them hear themselves before a real call.
Place a call
Browsers only let a page use the microphone and play sound after a user action, so start calls from a click. Each call takes its own microphone, speaker and volumes:
callButton.onclick = async () => {
const call = await phone.call({
to: numberInput.value,
input: micSelect.value,
output: phone.canChooseOutput ? speakerSelect.value : undefined,
inputVolume: Number(micVolume.value), // 0 to 2, 1 = unchanged
outputVolume: Number(speakerVolume.value),
});
call.on('state', (state) => (callStateEl.textContent = state)); // dialing, ringing, active, ended
call.on('level', ({ input, output }) => drawMeters(input, output));
call.on('ended', (reason) => console.log('call ended:', reason));
// everything can change mid-call
micSelect.onchange = () => call.setInput(micSelect.value);
speakerSelect.onchange = () => call.setOutput(speakerSelect.value);
micVolume.oninput = () => call.setInputVolume(Number(micVolume.value));
speakerVolume.oninput = () => call.setOutputVolume(Number(speakerVolume.value));
hangupButton.onclick = () => call.hangup();
muteButton.onclick = () => call.mute(!call.muted);
};With several lines, pass lineId; phone.lines lists them with their state. Without input or output, the call
uses the devices of the previous call, else the system defaults.
Answer incoming calls
answer() takes the same audio options:
phone.on('incoming', (call) => {
showRinging(call.from);
answerButton.onclick = () =>
call.answer({ input: micSelect.value, output: phone.canChooseOutput ? speakerSelect.value : undefined });
declineButton.onclick = () => call.hangup();
call.on('ended', hideRinging);
});SMS
The SDK is for calls. Incoming SMS and delivery reports reach your server through webhooks, and sending is a REST call your server makes with its API key.
Handling errors
Every failure is a PhoneError with a code. The ones a softphone should handle:
try {
await phone.call({ to });
} catch (err) {
if (err.code === 'microphone_denied') return showMicHelp();
if (err.code === 'line_busy') return showMessage('Already on a call');
throw err;
}The full list is in the SDK reference.