## The dialer session `ProjectBlueDialer` is one calling session for one line. Create it with a `tokenProvider`, call `start()`, and keep it alive for as long as the user can place or receive calls. ### Options - `tokenProvider` (() => Promise, required) — Called whenever the SDK needs a token: at start(), before expiry, and before a token's call limit is reached. Must call your own server. Return the mint response body as-is (recommended), or just the token string. To surface a refusal, throw mintErrorFromResponse(status, body) — or return the error body with its HTTP status added. - `refreshLeadSeconds` (number) — Refresh the token this many seconds before it expires. Minimum 5. Tokens live 15 minutes, so keep it well under that. - `logger` (console-like | null) — Where the SDK logs. Pass an object with debug/info/warn/error, or null to silence it entirely. - `edge` (string | string[]) — Preferred media region, e.g. "ashburn" or ["ashburn", "roaming"]. Leave unset unless you have measured a reason to pin one. - `playIncomingRingtone` (boolean) — Play a ringtone in the browser when an inbound call arrives. Set false to render your own. ### Writing the tokenProvider It runs whenever the SDK needs a token: at `start()`, about a minute before the current token expires, and before a token has been used for its tenth call. It must call **your** server, never the mint directly. Return the mint body unchanged. To report a refusal, throw `mintErrorFromResponse(res.status, body)` — it returns `null` on a 2xx and the matching typed error otherwise, so the three-line pattern in the rail is the whole thing. If you would rather return the error body, add `status` to it first; without it the SDK reports `402` for a cap refusal and `400` for everything else. ### Methods | Method | Returns | Notes | | --- | --- | --- | | `start()` | `Promise` | Fetches the first token and connects. Resolves once calls can be placed. Calling it twice is a no-op. | | `call(to)` | `Promise` | Places an outbound call. `to` must be E.164 **with** the leading `+`; spaces, dashes and parentheses are stripped, but a country code is never guessed. One call at a time — a second `call()` while one is active throws `invalid_state`. | | `refreshToken()` | `Promise` | Forces a refresh now. Usually unnecessary; concurrent calls share one in-flight refresh. | | `destroy()` | `Promise` | Hangs up any call, stops refreshing, releases the microphone, and removes all listeners. Idempotent. Call it on logout or unmount. | | `isReady()` | `boolean` | Started, not destroyed, and holding a token. | ### Getters | Property | Type | Notes | | --- | --- | --- | | `allowance` | `DialerAllowance \| null` | The latest allowance the server sent with a token. Null before `start()`. | | `inboundEnabled` | `boolean` | Whether the current token lets this session receive calls. | | `activeCall` | `DialerCall \| null` | The call in progress, if any. | ### Events Subscribe with `dialer.on(event, handler)`; `off` and `once` work the same way. | Event | Payload | When | | --- | --- | --- | | `ready` | `{ inboundEnabled }` | Calls can be placed. Fires again after every successful token refresh, with the current inbound state. | | `allowance` | `DialerAllowance` | A fresh allowance arrived with a token. Warn users on state: "soft". | | `capReached` | `DialerCapReachedError` | The workspace or line hit its cap. No new tokens until periodEnd; a call in progress continues. | | `incoming` | `DialerCall` | Someone is calling the line this session is scoped to. Call accept() or reject(). | | `tokenRefreshFailed` | `DialerError` | A background refresh failed. The session keeps its current token until it expires and retries with backoff. | | `error` | `DialerError` | Session-level error, e.g. inbound registration failed. Calls carry their own error events. | | `destroyed` | `—` | destroy() completed. | ### Token refresh, in detail You never manage tokens directly, but knowing the schedule explains what your server will see. The session refreshes 60 seconds before expiry by default, immediately before the call that would exceed `maxCallsPerToken`, and whenever the voice service reports the token invalid. A failed refresh emits `tokenRefreshFailed` and retries after 5, 15, then 45 seconds; the current token keeps working until it expires. A `cap_reached` refusal is different: the session emits `capReached`, stops retrying, and wakes itself one second after `periodEnd`. A call that is already up is never cut off — caps apply at mint time only. **Start a session — browser.ts** ```javascript import { ProjectBlueDialer, mintErrorFromResponse } from '@tryprojectblue/dialer'; const dialer = new ProjectBlueDialer({ async tokenProvider() { // Calls YOUR server, never Project Blue directly. const res = await fetch('/dialer-token', { method: 'POST' }); const body = await res.json(); const error = mintErrorFromResponse(res.status, body); if (error) throw error; return body; }, }); dialer.on('ready', ({ inboundEnabled }) => setStatus(inboundEnabled ? 'Ready' : 'Ready (outbound only)')); dialer.on('allowance', (a) => { if (a.state === 'soft') showBanner(`${a.pct}% of today's minutes used`); }); dialer.on('capReached', (error) => showBanner(error.message)); await dialer.start(); ``` **Tear down — browser.ts** ```javascript // On logout, route change or component unmount. // Hangs up any call, stops refreshing, releases the microphone. Idempotent. await dialer.destroy(); ```