## Voice Calling SDK Place and receive calls on your Project Blue lines from your own web app with `@tryprojectblue/dialer`. Your API key stays on your server; the browser holds only a short-lived token scoped to one line. FaceTime Audio is a separate, native API — see [FaceTime Audio](https://api.tryprojectblue.com/#facetime). This section covers regular phone calls through the Project Blue dialer. Phone numbers and caller ID are managed by Project Blue; you never touch a carrier account. ### How it works 1. **Your server** calls [the token mint](https://api.tryprojectblue.com/#dialer-token) with your API key and a `lineId`, and forwards the response to the browser. 2. **The SDK** takes a `tokenProvider` that calls your server. It handles refresh, the call lifecycle, inbound registration, and your plan's daily minute allowance. 3. **Your UI** calls `dialer.call(number)` and listens to events on the returned call. > **Two hosts, on purpose** > > Lines come from the messaging API at `api.tryprojectblue.com/get-lines`. Tokens come from the app at `app.tryprojectblue.com/api/v1/dialer/token`. The same API key works on both. Everything else in this reference lives on the first host; only the token mint lives on the second. ### Before you start | You need | Where | | --- | --- | | An API key | Dashboard → Settings → API Keys. Server-side only. Never ship it in a browser bundle or a mobile app. | | A Dialer subscription | Dashboard → Dialer. Tokens are refused with `dialer_subscription_required` until a plan is active. | | Approved business registration | Settings → Phone Compliance. Until approved, the mint answers `dialer_not_provisioned`. | | A paid line | Trial and shared lines cannot place SDK calls. Inbound-only lines cannot either. | | A browser | The SDK is browser-only: it needs WebRTC and a microphone. Ships ESM and CommonJS with TypeScript types. | The Settings → API Keys page in the dashboard shows these same steps with your account's status filled in, so it is the fastest way to see which prerequisite is missing. **Install — npm** ```bash npm install @tryprojectblue/dialer ``` **The whole loop — server.js** Your server holds the API key and proxies the mint. ```javascript // Your server. The API key never leaves it. app.post('/dialer-token', requireLogin, async (req, res) => { const upstream = await fetch('https://app.tryprojectblue.com/api/v1/dialer/token', { method: 'POST', headers: { Authorization: `Bearer ${process.env.PROJECT_BLUE_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ lineId: req.user.projectBlueLineId }), }); // Forward status, body and the rate-limit hint as-is; the SDK understands them. // Never let a non-JSON 502/503 from a proxy throw. const retryAfter = upstream.headers.get('retry-after'); if (retryAfter) res.set('Retry-After', retryAfter); const body = await upstream .json() .catch(() => ({ error: 'Token service unavailable', code: 'dialer_unavailable' })); res.status(upstream.status).json(body); }); ``` **The whole loop — browser.ts** The browser only ever sees a 15-minute, one-line token. ```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(); ```