06 Line

Call with the API

Place and answer calls with REST and a WebRTC session, from any language or platform.

In a browser, use the SDK

Call with the SDK does all of this for you, audio included. This page is for those who drive the calls themselves: another language, a server, a native app. Both follow the same steps.

A call needs two things: REST to place, answer and hang up, and a WebRTC session that carries the sound and reports what happens to the calls on its data channel.

Get a key

On a server, use your API key. In a browser or an app, never ship it: 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 answer holds the key, valid 15 minutes, and the ice_servers the WebRTC session needs. Ask for a new one about a minute before it expires.

Open a WebRTC session

Create a peer connection with the ice_servers, add one audio track and a data channel labelled 06line, gather every ICE candidate, then send the offer:

const { ephemeral_key: key, ice_servers: iceServers } = await fetch('/api/06line-key', { method: 'POST' }).then((r) => r.json());

const pc = new RTCPeerConnection({ iceServers });
pc.addTrack(microphoneTrack);
const events = pc.createDataChannel('06line');

await pc.setLocalDescription(await pc.createOffer());
await new Promise((done) => (pc.onicegatheringstatechange = () => pc.iceGatheringState === 'complete' && done()));

const res = await fetch('https://api.06line.com/v1/webrtc/sessions', {
  method: 'POST',
  headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ sdp: pc.localDescription.sdp }),
});
const session = await res.json(); // { id: "rtc_…", sdp }
await pc.setRemoteDescription({ type: 'answer', sdp: session.sdp });

There is no trickle ICE: the offer must hold every candidate. Audio is G.711 µ-law (PCMU), 8 kHz, which every WebRTC stack supports; keep the offer's default codecs and 06 Line picks it. An offer without PCMU gets no audio.

Keep the session open as long as the user may call or be called: it carries the audio of one call at a time, call after call. Close it with DELETE /v1/webrtc/sessions/{id} or by closing the peer connection: its call, if any, ends hangup. A session lost on the way (its network dropped and ICE failed) ends its call failed; open a new one.

06 Line then sends JSON messages on the 06line channel; it is for receiving only, actions go through REST:

typedataWhen
session.ready{ "lines": Line[] }Right after the channel opens: the lines this session can use
line.statea LineA line went online or offline, or its signal changed (checked every 5 seconds)
call.incominga CallA call is ringing on one of the lines; also right after session.ready for a call already ringing
call.statea CallA call of this session changed state, or a call that rang here stopped ringing
events.onmessage = ({ data }) => {
  const { type, data: payload } = JSON.parse(data);
  if (type === 'session.ready') showLines(payload.lines);
  if (type === 'line.state') updateLine(payload);
  if (type === 'call.incoming') ring(payload);
  if (type === 'call.state') updateCall(payload); // dialing, ringing, active, ended
};

Let the user pick devices

The microphone is the track you add to the session; the speaker is where you play the track 06 Line sends back. To change either mid-call, swap the track or the output, the session stays as it is:

const mic = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: micSelect.value, echoCancellation: true } });
const microphoneTrack = mic.getAudioTracks()[0];

pc.ontrack = ({ streams }) => {
  speaker.srcObject = streams[0]; // an <audio> element
  void speaker.play(); // after a user action: browsers block sound before one
};

micSelect.onchange = async () => {
  const next = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: micSelect.value } });
  await pc.getSenders()[0].replaceTrack(next.getAudioTracks()[0]);
};
speakerSelect.onchange = () => speaker.setSinkId?.(speakerSelect.value); // not every browser can choose the output
muteButton.onclick = () => (microphoneTrack.enabled = !microphoneTrack.enabled);

navigator.mediaDevices.enumerateDevices() lists the devices; their names stay empty until the user has granted microphone access once.

Place a call

curl -X POST https://api.06line.com/v1/calls \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"line_id": "line_…", "to": "+33698765432", "session_id": "rtc_…"}'

The call starts in dialing; call.state then reports ringing, active and ended. If the line cannot dial, the response (still 201) already holds the call ended, with end_reason failed or line_offline. An unknown session or a busy line is refused (404, 409) before anything is created.

Like SMS, a call takes an optional client_ref: the same request sent again returns the call already placed, with 200, instead of dialing twice.

To hang up, POST /v1/calls/{id}/hangup. Hanging up a call already ended is not an error: it returns the call as it is.

Answer incoming calls

Every open session of a company receives call.incoming (a session opened with a key restricted to a line, only for that line): ring everywhere, and answer with the session that should carry the sound:

curl -X POST https://api.06line.com/v1/calls/call_…/answer \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"session_id": "rtc_…"}'

The response holds the call still ringing: it turns active with the call.state that follows, once the line picked up. To decline, hang it up while it rings.

Every session it rang then receives call.state when it stops ringing: active once a session answered it (if it was not yours, someone else took it: stop ringing), or ended if the caller gave up or nobody answered. From then on, only the session that answered hears about the call. A late answer fails with line_busy if another session took the call, or call_not_active if it stopped ringing.

How a call ends

end_reasonMeaning
hangupYou hung up
remote_hangupThe other party hung up
no_answerNobody answered
busyThe other party was busy
line_offlineYour line lost the network
failedAny other failure, including the WebRTC session of the call lost on the way

One call at a time per line and per session: a second one fails with line_busy.

Recordings

Every call is recorded, from the moment it is placed or answered until it ends, as up to two mono WAV files (PCM 16-bit, 8 kHz), one per speaker that made a sound; an incoming call nobody answered has none:

Both start at the same moment, so they line up: play them together, or transcribe each speaker on its own. They are ready a few seconds after the call ends.

Rather than polling, subscribe a webhook endpoint to call.recorded: it arrives once the files are stored, with the call and a presigned link to each track there is, valid 7 days and usable without your API key, ready to hand to a transcription service. A call with no file sends no call.recorded.

In most countries, including France, you must tell the other party that the call is recorded, and why, before recording it. Say it at the start of the call, or play a message.

Native apps

Nothing here is specific to browsers: the same session and REST calls work from any WebRTC stack.

PlatformWebRTC library
iOS (Swift)WebRTC.framework (Google's libwebrtc)
Android (Kotlin, Java, Compose)org.webrtc (libwebrtc)
React Nativereact-native-webrtc
Flutterflutter_webrtc
ServersPion (Go), aiortc (Python), libdatachannel (C++)

The data channel only lives while your app runs. Ringing a phone whose app is closed needs a push notification (PushKit and CallKit on iOS, FCM and ConnectionService on Android); 06 Line does not send those yet.

History

GET /v1/calls lists past and current calls, newest first, filtered by line_id and/or peer (the other party's number), exactly like SMS history.

Handling errors

Failures come back as an error with a code. The ones a calling app should handle:

codeWhat to do
line_offlineThe line has no network: show it, retry later
line_busyA call is already on the line or the session, or another session answered first
call_not_activeThe call stopped ringing before your answer
not_foundUnknown call or session (or one of another line, with a key restricted to a line): open a new session
rate_limitedWait retry_after_ms, then retry

On this page