authkey.io
v1.3.1
Published
Unified voice calling SDK for the Authkey platform — one authkey, one call() API, browser (WebRTC) and Node.js (SIP/WebRTC via werift) auto-detected.
Maintainers
Readme
authkey.io
Unified voice calling SDK for the Authkey platform. One authkey, one call()
API, browser and Node.js auto-detected.
const { voice } = require("authkey.io"); // or: import { voice } from "authkey.io"
const client = new voice({ authkey: "YOUR_AUTHKEY" });
client.on("incoming", async (call) => {
await call.answer();
});
await client.connect();
const call = await client.call({ channel: "direct", from: "+9188xxxxxxx", to: "+919999999999" });Install
npm install authkey.ioBundlers that respect the package exports "browser" condition (Webpack,
Rollup, Vite, esbuild) pull in the browser build automatically for a browser
target; plain require("authkey.io") in Node resolves to the Node build.
Channels
Only "direct" is available in this release — a real phone number with a
real backend behind it. "whatsapp" is a recognized constant but calling
client.call({channel: "whatsapp", ...}) throws ChannelNotAvailableError:
WhatsApp calling isn't available for external accounts yet.
How calls work
- Inbound:
client.on("incoming", call => ...)fires as soon as someone dials your number. Callcall.answer()to pick up,call.reject()to decline. If the call reached your number because it was forwarded there by an upstream carrier/PBX,call.diversionis{ from, reason }(the diverting party's SIP URI and the forwarding reason, e.g."unconditional"/"no-answer"/"user-busy"); otherwise it'snull. - Outbound:
await client.call({ from, to })places the call and resolves once it connects. PassringTimeoutMs(default 30000) to control how long to wait.
Once a call is answered/connected, use call.hangup(), call.hold(),
call.resume(), call.mute(), call.unmute(), and call.sendDTMF("1234")
the same way on either side.
Playing and receiving audio (Node.js)
Node has no microphone or speaker, so instead of native browser audio, this SDK exposes calls as raw PCM: 16-bit, mono, 16kHz, in 20ms frames (640 bytes each). You send audio by writing PCM buffers to the call; you receive audio by listening for PCM buffers as they arrive.
Requires ffmpeg on your system PATH (used to transcode your PCM to
the codec the call actually negotiated, and back). If your ffmpeg binary
isn't on PATH, set AUTHKEY_FFMPEG_PATH to its full path.
Echo cancellation is on by default. If you're building something that
listens to on('audio') while also calling sendAudio() (e.g. an AI voice
agent doing STT while it talks), the far end's own line/handset can echo
your played audio back — this SDK adaptively cancels that out before you
ever see it. Set AUTHKEY_ECHO_CANCEL=false to disable it, or tune
AUTHKEY_ECHO_REF_DELAY_MS (default 300) / AUTHKEY_ECHO_FILTER_MS
(default 60) if you're still seeing bleed-through — the right values
depend on your actual call path's round-trip latency, so some tuning
against a real call is expected. It also self-protects: it measures its own
CPU cost against real time and permanently falls back to plain passthrough
for that call if it can't keep up (tune the threshold with
AUTHKEY_ECHO_MAX_LOAD, default 0.4) — so a slower deployment or a lot of
concurrent calls degrades to "no echo cancellation" for that call, never to
one-way or dropped audio.
Inbound audio is decoded with wallclock-based timestamps (ffmpeg's
-use_wallclock_as_timestamps) rather than trying to derive timing from raw
RTP-over-UDP alone, since the latter can sound robotic or drift out of sync.
Long-running calls and SIP re-INVITEs: a SIP session-timer refresh can
arrive as a re-INVITE partway through a call. The SDK now safely tears down
and restarts its own inbound pipeline if that happens (no leaked ffmpeg
processes/sockets across restarts). If your deployment's re-INVITEs also
change the RTP SSRC (FreeSWITCH does this in some configurations) and you
see audio go dead partway through long calls specifically, see
autoLatchSsrcOnReinvite under Node.js constructor options below.
Playing a sound file into a call
The simplest way to play an audio file (a greeting, an IVR prompt, hold
music) is to convert it to raw PCM once with ffmpeg, then stream it in
20ms chunks:
# One-time conversion — any input format ffmpeg supports (mp3, wav, etc.)
ffmpeg -i greeting.mp3 -f s16le -ar 16000 -ac 1 greeting.pcmconst fs = require("fs");
async function playFile(call, pcmPath) {
const pcm = fs.readFileSync(pcmPath);
const FRAME_BYTES = 640; // 20ms @ 16kHz, 16-bit, mono
for (let offset = 0; offset < pcm.length; offset += FRAME_BYTES) {
if (call.isTerminal) break; // caller hung up mid-playback
call.sendAudio(pcm.subarray(offset, offset + FRAME_BYTES));
await new Promise((r) => setTimeout(r, 20)); // pace to real time, don't burst
}
}
client.on("incoming", async (call) => {
await call.answer();
await playFile(call, "./greeting.pcm");
});The 20ms pacing is important — sendAudio() expects frames roughly every
20ms, matching real-time playback speed. Sending everything at once will not
work.
Receiving what the caller says
call.on("audio", (pcmChunk) => {
// pcmChunk is raw 16-bit/mono/16kHz PCM — write it to a file, feed it to
// a speech-to-text engine, whatever you need.
});Text-to-speech on the fly
Any TTS engine that can output 16-bit/16kHz/mono PCM (or anything ffmpeg
can read) works the same way — generate the audio, convert it with ffmpeg
if needed, then stream it with the same chunking loop as above.
API reference
client.on("incoming", (call) => {});
client.on("connected" | "connecting" | "disconnected" | "reconnecting" | "reconnected" | "authenticationFailed", () => {});
const call = await client.call({ channel: "direct", from, to, ringTimeoutMs: 30000 });
call.id; call.channel; call.from; call.to; call.status; call.startTime; call.endTime;
await call.answer();
await call.reject();
await call.hangup();
await call.hold();
await call.resume();
await call.mute();
await call.unmute();
await call.sendDTMF("1234");
// Node.js only:
call.on("audio", (pcm) => {});
call.sendAudio(buffer);Reporting
// same authkey, REST only, works from any runtime
const { RestClient } = require("authkey.io");
const rest = new RestClient("YOUR_AUTHKEY");
await rest.getCdrReport({ from: "2026-08-01", to: "2026-08-10" });
await rest.getSummaryReport();
await rest.getRecordingsReport();
await rest.getUsageReport();Node.js constructor options
const client = new voice({
authkey: "YOUR_AUTHKEY",
iceServers: [{ urls: "stun:stun.l.google.com:19302" }], // or your own TURN server
autoLatchSsrcOnReinvite: false, // opt-in, see below
});iceServers— standard WebRTCRTCIceServer[]. If your Node process runs behind a NAT/firewall (most cloud deployments), pass at least a STUN server here — without it, ICE can only gather host candidates, which breaks inbound audio delivery for exactly that deployment shape. Defaults to[].autoLatchSsrcOnReinvite— opt-in, defaults tofalse. Some SIP session-timer re-INVITEs (FreeSWITCH's included) change the RTP stream's SSRC mid-call; werift silently drops every packet on an SSRC it hasn't seen before, which can make a call go one-way-silent partway through with no error. Turning this on skips renegotiating on any re-INVITE that arrives after the call is already established, and instead just registers the new SSRC against the existing audio receiver. This is the right fix for a pure session-timer refresh, but wrong for a genuine hold/resume or codec change riding on a re-INVITE — this SDK can't reliably tell those apart from the SDP alone, so it's off by default. Only turn it on if you've confirmed your deployment's re-INVITEs are pure timer refreshes.
Known limitations (Node.js path)
- DTMF send is not implemented.
call.sendDTMF()emits thedtmfevent locally but doesn't send tones over the wire yet. - Hold/resume are local-only.
call.hold()/.resume()stop and restart your own audio flow; they don't yet renegotiate the call itself. - Each active Node.js call spawns
ffmpegprocesses for audio transcoding. This is fine at moderate call volumes; very high concurrent call counts will need a lower-overhead audio path in a future release.
Testing
npm test