Documentation

@alloqui/dialer

Drop-in React dialer for any telephony provider. One component, your provider keys, zero backend.

npmMIT licenseReact 18 / 19
On this page

Overview

Alloqui is a hosted voice platform — "Firebase for Voice". You bring your own telephony provider keys (Twilio or Plivo), and Alloqui handles token generation, webhook routing, call control, and real-time events behind a single project key.

@alloqui/dialer is the client side of that platform: a drop-in React dialer component with a dial pad, call timer, hold, mute, and DTMF — plus a headless engine and a hook when you want to build your own UI.

import { Dialer } from "@alloqui/dialer";

<Dialer projectKey="al_live_abc123" />

Call audio flows directly between the browser and your provider over WebRTC. Alloqui only handles signaling — your calls never touch our servers.

Quick start

  1. Sign up at alloqui.dev and create a project.
  2. Enter your provider credentials (Twilio or Plivo) in the dashboard. Alloqui encrypts them with AES-256-GCM and auto-configures the provider side — TwiML apps, API keys, and webhooks.
  3. Copy your project key (al_live_...) from the project page.
  4. Drop the component into your app and make your first call.

The project key is the only credential your frontend ever sees. Provider secrets stay encrypted on Alloqui's servers.

Installation

Install the package along with its required peer dependencies:

npm install @alloqui/dialer lucide-react sonner

Then add the SDK for your provider (only the one your project uses):

# Twilio projects
npm install @twilio/voice-sdk

# Plivo projects
npm install plivo-browser-sdk

Provider SDKs are optional peer dependencies. At runtime the dialer asks the Alloqui API which provider your project key belongs to and lazy-loads the matching engine — so only the SDK you installed is ever bundled and shipped to the browser.

Styles are injected automatically when the package is imported. There is no CSS file to import.

Requirements: React 18 or 19, a secure context (HTTPS or localhost — WebRTC requires it), and microphone permission from the user.

Usage

The fastest path is the floating dialer — a launcher bubble that expands into a dial pad:

import { Dialer } from "@alloqui/dialer";

export function App() {
  return (
    <>
      {/* your app */}
      <Dialer projectKey="al_live_abc123" />
    </>
  );
}

Prefer an always-visible panel? Use mode="panel" to render the dialer inline (360 × 640 px):

<Dialer projectKey="al_live_abc123" mode="panel" theme="dark" />

Listen to call lifecycle events with callbacks:

<Dialer
  projectKey="al_live_abc123"
  contacts={{ "+61412345678": "Alice Chen" }}
  onCallStart={(call) => console.log("dialing", call.phoneNumber)}
  onCallEnd={(call) => console.log("ended after", call.duration, "ms")}
  onError={(error) => console.error(error)}
/>

Next.js note: the dialer uses WebRTC and portals into document.body, so it is client-only. In the App Router, render it from a component marked "use client".

Dialer props

PropTypeDefaultDescription
projectKeystring— (required)Your Alloqui project key (al_live_...).
apiBaseUrlstringAlloqui cloud APIOverride the API endpoint. Only needed for self-hosted or staging setups.
mode"float" | "panel""float"float renders a launcher bubble portaled to document.body; panel renders the dialer inline.
position"bottom-right" | "bottom-left" | "top-right" | "top-left""bottom-right"Corner for the floating bubble (float mode only).
theme"light" | "dark" | "auto""auto"Color scheme. auto follows your site's dark-mode class — see Theming.
contactsRecord<string, string>Map of phone number → display name. Numbers must match the dialed string exactly; matches show the name on call toasts.
onCallStart(call: CallInfo) => voidFires when a call starts dialing (not when it connects — use the call.connected event for that).
onCallEnd(call: CallInfo) => voidFires when a call ends or disconnects.
onError(error: Error) => voidFires on initialization or token errors.

While a call is connected, pressing dial-pad digits sends DTMF tones instead of editing the number.

useDialer hook

For custom UIs, useDialer gives you the full dialer state and control surface without any of the built-in components:

import { useDialer, CallState, DialerState } from "@alloqui/dialer";

function MyDialer() {
  const {
    dialerState,   // "initializing" | "ready" | "error"
    callState,     // "idle" | "dialing" | "ringing" | "connected" | ...
    call,          // active CallInfo or null
    muted,
    held,
    lastCall,
    lastError,
    makeCall,
    hangup,
    toggleMute,
    toggleHold,
    sendDTMF,
    redial,
  } = useDialer({ projectKey: "al_live_abc123" });

  if (dialerState !== DialerState.Ready) return <p>Connecting…</p>;

  return callState === CallState.Idle ? (
    <button onClick={() => makeCall("+61412345678")}>Call Alice</button>
  ) : (
    <button onClick={hangup}>
      Hang up {call?.phoneNumber} ({Math.round((call?.duration ?? 0) / 1000)}s)
    </button>
  );
}

The hook accepts the same config as the component's core props: projectKey, apiBaseUrl, onCallStart, onCallEnd, onError. It re-initializes only when projectKey or apiBaseUrl changes — callback identity changes never tear down an active call. makeCall rejects if the dialer isn't initialized yet, so gate your call buttons on dialerState.

Headless engine

AlloquiDialer is the framework-agnostic engine underneath both the component and the hook. Use it outside React, or when you need fine-grained event subscriptions:

import { AlloquiDialer } from "@alloqui/dialer";

const dialer = new AlloquiDialer({ projectKey: "al_live_abc123" });

const unsubscribe = dialer.on("call.connected", ({ call }) => {
  console.log("connected to", call.phoneNumber);
});

dialer.on("dialer.ready", async () => {
  await dialer.call("+61412345678");
});

// later
dialer.hangup();
unsubscribe();
dialer.destroy();

Methods: call(phoneNumber), hangup(), toggleMute(), toggleHold(), sendDTMF(digit), redial(), getSnapshot(), destroy(), plus on(event, listener) (returns an unsubscribe function) and off(event, listener).

toggleMute, toggleHold, and sendDTMF are no-ops unless a call is connected. call() throws if the dialer isn't ready or a call is already in progress.

Events

Subscribe to any of these via dialer.on(...):

EventPayloadWhen
dialer.readyEngine initialized and ready to place calls.
dialer.error{ error: Error }Initialization or token refresh failed.
call.dialing{ call: CallInfo }Outbound call started.
call.ringing{ call: CallInfo }Remote side is ringing.
call.connected{ call: CallInfo }Call answered; audio flowing.
call.ended{ call: CallInfo }Call ended normally.
call.disconnected{ call: CallInfo }Call dropped due to an error.
call.muted{ muted: boolean }Mute toggled.
call.held{ held: boolean }Hold toggled.

Types

Everything is fully typed. The key exports:

import {
  CallState,      // Idle | Dialing | Ringing | Connected | Ended | Disconnected
  CallDirection,  // Outbound | Inbound
  DialerState,    // Initializing | Ready | Error
} from "@alloqui/dialer";

import type {
  DialerProps,    // <Dialer /> props
  AlloquiConfig,  // useDialer / AlloquiDialer config
  CallInfo,       // call metadata (below)
  CallEventMap,   // event name → payload map
  DialerSnapshot, // useDialer's state shape
  DialerTheme,    // "light" | "dark" | "auto"
  FloatPosition,  // "bottom-right" | "bottom-left" | "top-right" | "top-left"
} from "@alloqui/dialer";

CallInfo describes every call:

FieldTypeDescription
idstringUnique call id.
phoneNumberstringThe dialed number.
directionCallDirectionoutbound or inbound.
stateCallStateCurrent state of this call.
startedAtnumber | nullEpoch ms when the call connected.
endedAtnumber | nullEpoch ms when the call ended.
durationnumberConnected time in milliseconds, updated every second.

Theming

Two layers of control:

1. The theme prop"light", "dark", or "auto" (default). With auto, the dialer follows your site's dark mode: it picks up an ancestor .dark class, [data-theme="dark"], or [data-color-mode="dark"], so Tailwind and next-themes setups work with zero configuration. If your site has no dark-mode class, auto resolves to light.

2. CSS custom properties — override the design tokens on .alloqui-dialer to match your brand:

.alloqui-dialer {
  --alloqui-bg: #0b0b0e;
  --alloqui-surface: #17171b;
  --alloqui-surface-hover: #202028;
  --alloqui-text-primary: #f4f4f5;
  --alloqui-text-secondary: #a1a1aa;
  --alloqui-text-muted: #71717a;
  --alloqui-success: #22c55e;
  --alloqui-danger: #ef4444;
  --alloqui-radius-lg: 16px;
  --alloqui-font-body: "Inter", sans-serif;
}

Call toasts have their own scoped tokens (--alloqui-toast-bg, --alloqui-toast-btn-bg, --alloqui-toast-number, --alloqui-toast-timer, and per-state dot colors like --alloqui-toast-dot-connected) on .alloqui-toast.

How it works

  1. The dialer sends your project key to the Alloqui API (POST /api/v1/token, X-Project-Key header).
  2. The API resolves your project, decrypts your provider credentials server-side, and returns a short-lived access token plus the provider name.
  3. The dialer lazy-loads the matching voice engine (Twilio or Plivo) and initializes it with the token.
  4. Calls connect over WebRTC directly between the browser and your provider — Alloqui proxies signaling only, never media.
  5. Tokens refresh automatically in the background (with retry and backoff) for as long as the dialer is mounted.

Because provider credentials never leave Alloqui's encrypted vault, the project key is safe to use in frontend code — it can only mint short-lived voice tokens scoped to your project.

Free tier limits

The free tier includes outbound calling with:

LimitFree tier
Projects1
Outbound calls50 / day
Max call duration15 minutes
Concurrent calls1
Call history7 days

Inbound calls, transfer and conferencing, call recording, AI transcription, and premium themes are part of Pro at a flat $15/month — never per-minute.