{"_id":"@aladdin-ai/voicecall","name":"@aladdin-ai/voicecall","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@aladdin-ai/voicecall","version":"0.1.0","description":"A voice client for apps: register a device, ring another user, talk to a voice agent — no SIP anywhere.","license":"MIT","type":"module","sideEffects":["**/*.css"],"main":"./dist/cjs/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/cjs/index.js"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.js","require":"./dist/cjs/server.js"},"./client":{"types":"./dist/client.d.ts","import":"./dist/client.js","require":"./dist/cjs/client.js"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/cjs/react/index.js"},"./react/styles.css":"./dist/react/styles.css","./package.json":"./package.json"},"scripts":{"build":"node scripts/build.mjs","test":"npm run build && node --test 'test/*.test.mjs'","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"npm run test"},"keywords":["voice","voip","calling","webrtc","aladdin"],"dependencies":{"livekit-client":"^2.18.0"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"devDependencies":{"@types/react":"^19.2.7","react":"^19.2.1","typescript":"^5.9.3"},"publishConfig":{"access":"public"},"engines":{"node":">=18"},"_id":"@aladdin-ai/voicecall@0.1.0","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-tgiyyaHubIU5QPw2lwtS3zCd1dDP36T5tm0MqpIy0Kl5qiEDG5CyNZ8b9lyX85X+/3cWFX+IhOkl+QU5tvunzw==","shasum":"6d744420b76b53661eabe7285b596766bd86785d","tarball":"https://registry.npmjs.org/@aladdin-ai/voicecall/-/voicecall-0.1.0.tgz","fileCount":125,"unpackedSize":492488,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIALIfOjAcESsmGobIkx1t/dwwPLE5RYEoM2+HAYGnVqeAiEAyMnNxsrf8rCSa/CQI15Vefoe7U40/EcwPdty1uB+2cc="}]},"_npmUser":{"name":"kittipong.pa","email":"kittipong.panchon@gmail.com"},"directories":{},"maintainers":[{"name":"kittipong.pa","email":"kittipong.panchon@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/voicecall_0.1.0_1788367219478_0.9045045107626388"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-02T16:40:19.297Z","0.1.0":"2026-09-02T16:40:19.669Z","modified":"2026-09-02T16:40:19.902Z"},"maintainers":[{"name":"kittipong.pa","email":"kittipong.panchon@gmail.com"}],"description":"A voice client for apps: register a device, ring another user, talk to a voice agent — no SIP anywhere.","keywords":["voice","voip","calling","webrtc","aladdin"],"license":"MIT","readme":"# @aladdin-ai/voicecall\n\nA voice client for apps. Register a device, ring another user, talk to a voice agent — no SIP anywhere.\n\n```bash\nnpm install @aladdin-ai/voicecall\n```\n\n`react` is an optional peer dependency, needed only for `@aladdin-ai/voicecall/react`.\n\n## Entry points\n\n| import | for | pulls in |\n| --- | --- | --- |\n| `@aladdin-ai/voicecall/server` | your backend | nothing but `fetch` |\n| `@aladdin-ai/voicecall` | the app | the client, and the audio stack on the first call |\n| `@aladdin-ai/voicecall/react` | a React app | the above, plus the provider and screens |\n| `@aladdin-ai/voicecall/react/styles.css` | those screens | — |\n\nBoth ESM and CommonJS are published, with types for each.\n\n## The two halves\n\nThe SDK is split where the credentials split.\n\n| | runs | holds | does |\n| --- | --- | --- | --- |\n| `AladdinVoiceServer` | your backend | the tenant key `sk_…` | register a device, mint its session, place a call |\n| `AladdinVoiceClient` | the app | that device's session token | the event connection, and the one call a phone can have |\n\nThe tenant key opens every tenant route for every user, so it never reaches a browser — `AladdinVoiceServer` refuses to construct in one. Hand the device its session token and nothing else.\n\n```ts\n// your server\nimport { AladdinVoiceServer } from '@aladdin-ai/voicecall/server';\n\nconst voice = new AladdinVoiceServer({ apiKey: process.env.ALADDIN_API_KEY!, environment: 'live' });\nconst client = await voice.registerClient({ userId, deviceId, label: 'iPhone' });\nconst session = await voice.createSession({ deviceId });\n```\n\n```ts\n// the app\nimport { AladdinVoiceClient } from '@aladdin-ai/voicecall';\n\nconst phone = new AladdinVoiceClient({\n  environment: 'live',\n  // A function, not a string: sessions expire, and this is called again\n  // whenever the gateway rejects the one it has.\n  sessionToken: () => fetch('/api/voice/session').then((r) => r.json()).then((s) => s.session_token),\n  placeCall: (input) => fetch('/api/calls', { method: 'POST', body: JSON.stringify(input) }).then((r) => r.json()),\n});\n\nphone.subscribe(() => render(phone.getState()));\nawait phone.start();\nawait phone.dial({ toPhoneNumber: '00000123' });\n```\n\nA call is a room you join. There is no hangup endpoint: leaving the room is hanging up.\n\n## Environments\n\nTwo gateways, two hostnames. Keys are not portable between them.\n\n| `environment` | gateway |\n| --- | --- |\n| `'test'` *(default)* | `https://api.getaladdin.dev` |\n| `'live'` | `https://api.getaladdin.io` |\n\nResolution order, most specific first:\n\n1. `baseUrl` — a proxy of your own, or a gateway that is neither public one\n2. `environment` — `'test'` or `'live'`\n3. `ALADDIN_BASE_URL`, then `ALADDIN_ENV`\n4. `test`, because a default that bills is rude\n\nEnv vars are read wherever `process.env` exists. In a browser bundle only `NEXT_PUBLIC_ALADDIN_BASE_URL` and `NEXT_PUBLIC_ALADDIN_ENV` can be inlined by the bundler, so those two are read as well — or pass `environment` explicitly and skip the question.\n\n`ALADDIN_ENV` also accepts `production` / `prod` / `io` for live and `sandbox` / `dev` / `staging` for test. Anything else throws at construction rather than falling back, because a typo that silently means test is a live app that never rings.\n\n## `AladdinVoiceServer` options\n\n| option | default | what it does |\n| --- | --- | --- |\n| `apiKey` | `ALADDIN_API_KEY` | the tenant key, `sk_…`. Required, from here or the env |\n| `environment` / `baseUrl` | see above | which gateway |\n| `timeoutMs` | `20000` | how long a gateway call may take |\n| `maxRetries` | `2` | extra attempts, for idempotent requests only |\n| `headers` | `{}` | sent on every request: a trace id, your own proxy auth |\n| `fetch` | global `fetch` | swap the transport — instrumentation, a test double |\n| `logger` | silent | where diagnostics go |\n\nMethods: `registerClient()`, `createSession()`, `placeCall()`, and a `baseUrl` getter worth logging at boot.\n\n`registerClient` is the only one retried for you. It is idempotent on `deviceId`; a repeat of `placeCall` after a timeout would be a second call to a real person, so that one is never repeated on your behalf.\n\n## `AladdinVoiceClient` options\n\nEverything above except `apiKey`, plus:\n\n| option | default | what it does |\n| --- | --- | --- |\n| `sessionToken` | — | the device credential, or a function that mints one. Required |\n| `placeCall` | — | places the call through your backend, which holds the tenant key. Required |\n| `sounds` | `true` | ringing: `false` to mute both, or the object below |\n| `volume` | `0.14` | shorthand for `sounds.volume` |\n| `endedLingerMs` | `2500` | how long the ended state stays before the phone returns to idle. `0` returns as soon as the app has seen it; `null` holds it until `reset()` |\n| `autoAck` | `true` | report ring delivery, so the caller can be told the phone lit up |\n| `deviceName` | `'aladdin-voicecall-sdk'` | the name this device reports on the event connection |\n| `reconnect` | `{ baseDelayMs: 500, maxDelayMs: 30000 }` | full-jitter backoff. `maxAttempts` stops retrying after N failures; unlimited by default |\n| `media` | see below | microphone, jitter buffer, diagnostics |\n| `onError` | — | anything unrecoverable-but-not-fatal: a denied microphone, a failed decline |\n| `onEvent` | — | every event this device receives, before the SDK acts on it |\n\nMethods: `start()`, `stop()`, `dial()`, `answer()`, `decline()`, `hangup()`, `setMuted()`, `resumeAudio()`, `refresh()`, `reset()`, `subscribe()`, `getState()`, and the sound controls below.\n\n### Staying reachable\n\nA registered device that is not connected does not ring, so the connection is defended:\n\n- it recovers what it missed while away, and drops the replays it already acted on;\n- a socket that stops delivering without closing is replaced — `readyState` still says OPEN in that state, and calls simply never arrive;\n- it reconnects the moment the browser says the network is back, rather than at the end of a backoff;\n- a refused credential ends the stream instead of retrying forever. That surfaces as `onError` with a `ConnectionError`, and the fix is a new session — which is why `sessionToken` takes a function.\n\n## Errors\n\nEvery failure is an `AladdinError` with a stable `code`, so nothing has to match on message text.\n\n| class | `code` | means |\n| --- | --- | --- |\n| `ApiError` | `api_error` / `unauthorized` | the gateway answered and said no. Carries `status`, `apiCode` |\n| `TimeoutError` | `timeout` | abandoned before the gateway answered |\n| `NetworkError` | `network_error` | never reached the gateway |\n| `ConnectionError` | `connection_error` | the event connection stopped for good |\n| `MediaError` | `media_error` | the audio leg failed |\n| `InvalidRequestError` | `invalid_request` | the SDK was called wrongly |\n\n`isRetryable(error)` and `isAuthError(error)` are exported for the two branches worth writing.\n\n## Sound\n\nRinging is on by default — a phone that rings silently is not a phone — and every part of it can be changed **while it rings**. A mute switch that only works before construction is not a mute switch.\n\n```ts\nphone.setSoundsEnabled(false);        // silence the phone, mid-ring if need be\nphone.setVolume(0.4);                 // 0–1\nphone.setRingtoneEnabled(false);      // incoming sound only\nphone.setRingbackEnabled(false);      // the caller's \"it is ringing over there\"\nphone.getSoundConfig();               // every setting, defaults filled in\n```\n\n`configureSounds()` takes the same shape as the `sounds` option, and patches merge — turning a sound off does not forget its cadence.\n\n### `SoundOptions`\n\n| field | default | what it does |\n| --- | --- | --- |\n| `enabled` | `true` | the master switch |\n| `volume` | `0.14` | 0–1, shared by both sounds |\n| `ringtone` | `true` | what a device plays when somebody is calling it. `false`, or a `SoundSpec` |\n| `ringback` | `true` | what a caller hears while the far end rings. `false`, or a `SoundSpec` |\n\n### `SoundSpec` — per sound\n\n| field | default | what it does |\n| --- | --- | --- |\n| `enabled` | `true` | silence this one and leave the other alone |\n| `src` | — | an audio file to play instead of the built-in synth |\n| `tones` | the real cadences | `{ frequency, duration, at? }[]`, one beat |\n| `intervalMs` | `5000` ringback / `4000` ringtone | ms between beats |\n| `volume` | inherits | 0–1 for this sound alone |\n| `loop` | `true` | loop a `src` file rather than restarting it every interval |\n\nThe defaults are the real cadences: ringback is 425 Hz, one second on and four off; the ringtone is a warble, because a ringing handset has to sound different from a call that is merely connecting. They are synthesised rather than shipped as files, so the phone still rings behind a strict CSP, on a slow first load, and offline.\n\n```ts\nnew AladdinVoiceClient({\n  …,\n  sounds: {\n    volume: 0.2,\n    ringback: false,                                   // your own progress UI\n    ringtone: { src: '/sounds/ring.mp3', volume: 0.5 },\n  },\n});\n```\n\nA `src` your CSP blocks is silence, which is why the default is not one.\n\n## `media` options\n\n| field | default | what it does |\n| --- | --- | --- |\n| `audioCapture` | echo cancellation, noise suppression, AGC | microphone constraints; `deviceId` picks the input |\n| `jitterBufferTargetMs` | `200` | audio held before playing. Lower is less latency and more concealment on a swinging path; `0` leaves the browser default |\n| `autoEnableMicrophone` | `true` | `false` joins listen-only; `setMuted(false)` turns it on later |\n| `audioHealthLog` | `false` | per-second concealment and packet loss through the logger, for chasing a stutter |\n\nA refused microphone does not end the call: the user can still hear, `onError` fires, and `call.micDenied` says why nobody can hear them.\n\n## Logging\n\nSilent by default — a library that writes to the console unasked shows up in somebody else's error tracking.\n\n```ts\nimport { consoleLogger } from '@aladdin-ai/voicecall';\n\nnew AladdinVoiceClient({ …, logger: consoleLogger('debug') });\n// or forward it anywhere\nnew AladdinVoiceClient({ …, logger: (level, message, meta) => tracker.log(level, message, meta) });\n```\n\n## React\n\n```tsx\nimport { AladdinVoiceProvider, CallScreen, IncomingCallScreen, Dialpad, useAladdinVoice } from '@aladdin-ai/voicecall/react';\nimport '@aladdin-ai/voicecall/react/styles.css';\n\n<AladdinVoiceProvider identity={identity} placeCall={placeCall} environment=\"live\" sounds={{ volume: 0.2 }}>\n  <YourApp />\n  <IncomingCallScreen />\n  <CallScreen />\n</AladdinVoiceProvider>;\n```\n\nThe provider takes **every client option as a prop**, plus:\n\n| prop | default | what it does |\n| --- | --- | --- |\n| `identity` | — | `null` until the device signs in; the provider idles until then |\n| `placeCall` | — | calls your backend |\n| `voiceAgentId` | — | what `callAgent()` dials |\n| `historyLimit` | `10` | how many finished calls `history` keeps |\n\nEndpoint props rebuild the client — pointing a live app at test mid-session is not a thing to do quietly. Sound props are applied to the running client instead.\n\n`useAladdinVoice()` returns `{ identity, call, connection, error, history, sounds, dial, callAgent, answer, decline, hangup, setMuted, resumeAudio, setSoundsEnabled, setVolume, configureSounds, reset, dismissError }`.\n\n`useVoiceSounds()` is the sound half on its own, for a mute button that should not redraw on every call state change it does not read:\n\n```tsx\nconst { sounds, setSoundsEnabled } = useVoiceSounds();\n\n<button aria-pressed={sounds?.enabled} onClick={() => setSoundsEnabled(!sounds?.enabled)}>\n  {sounds?.enabled ? 'Ring' : 'Silent'}\n</button>;\n```\n\nRead `sounds` rather than keeping a local `ringOn`: it is what the phone will actually do, including when something else changed it.\n\nOverride the `--arc-*` CSS variables and the screens look like your product.\n\n## Developing this package\n\n```bash\nnpm run typecheck\nnpm test          # builds, then runs the suite against dist\nnpm run build\n```\n\nReleases are described in [PUBLISHING.md](./PUBLISHING.md), changes in [CHANGELOG.md](./CHANGELOG.md).\n","readmeFilename":"README.md","_rev":"1-219a54e078ddff0aedd8229fab244c67"}