06 Line

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-browser

Give 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:

server: /api/06line-key
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, closed

Let 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 output

Device 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.

On this page