{"_id":"@3cx/call-control-sdk","_rev":"4-a3dd62038d0f2f4d6f46c7d1d8139245","name":"@3cx/call-control-sdk","dist-tags":{"latest":"0.1.10"},"versions":{"0.1.7":{"name":"@3cx/call-control-sdk","version":"0.1.7","keywords":["3cx","pbx","call-control","voip","telephony","sdk","ai","voice-agent"],"author":{"name":"3CX"},"license":"MIT","_id":"@3cx/call-control-sdk@0.1.7","maintainers":[{"name":"sherlock1982","email":"nick.orekhov@gmail.com"},{"name":"kireban","email":"kka@3cx.com"}],"dist":{"shasum":"1f46c8d58bf5cf3edd70cd840163fcd8132ce3c1","tarball":"https://registry.npmjs.org/@3cx/call-control-sdk/-/call-control-sdk-0.1.7.tgz","fileCount":33,"integrity":"sha512-1ECvaKNOmry6+78OToD61Wj+Rqk+6vL+uO84EaTarZqOeMJ7VH1isAtcuNh4XHypC3q31Xb3nck9EiK/Ba0bqw==","signatures":[{"sig":"MEUCIQC/t14EdTexhAsgUHBm1FBs57U41Rp2IV87CRYYMlEHJgIgFZTWuLvNmScaaYnq5n5SpCkoeeYWxtTHWsfFNqQc1tg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":85906},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"5938f4029aad0a8dd048e3b729687c23b710c62b","scripts":{"lint":"eslint .","test":"vitest run","build":"yarn clean && tsc","clean":"rimraf dist","lint:fix":"eslint . --fix","test:watch":"vitest","test:coverage":"vitest run --coverage --coverage.include=\"src/**/*.ts\"","prepublishOnly":"yarn build"},"_npmUser":{"name":"kireban","email":"kka@3cx.com"},"_npmVersion":"11.16.0","description":"SDK for interacting with the 3CX Call Control API","directories":{},"_nodeVersion":"24.18.0","dependencies":{"ws":"^8.21.1","axios":"^1.18.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"yarn@4.17.1","devDependencies":{"eslint":"^10.8.0","rimraf":"^6.1.3","vitest":"^4.1.10","@types/ws":"^8.18.1","@eslint/js":"^10.0.1","typescript":"6.0.3","@types/node":"^24.13.3","typescript-eslint":"^8.65.0","@vitest/coverage-v8":"^4.1.10"},"peerDependencies":{"@types/node":">=24"},"peerDependenciesMeta":{"@types/node":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/call-control-sdk_0.1.7_1785315969336_0.78305320307467","host":"s3://npm-registry-packages-npm-production"}},"0.1.8":{"name":"@3cx/call-control-sdk","version":"0.1.8","keywords":["3cx","pbx","call-control","voip","telephony","sdk","ai","voice-agent"],"author":{"name":"3CX"},"license":"MIT","_id":"@3cx/call-control-sdk@0.1.8","maintainers":[{"name":"sherlock1982","email":"nick.orekhov@gmail.com"},{"name":"kireban","email":"kka@3cx.com"}],"dist":{"shasum":"6a95296e50164322531fbb844ac95b2d53282521","tarball":"https://registry.npmjs.org/@3cx/call-control-sdk/-/call-control-sdk-0.1.8.tgz","fileCount":33,"integrity":"sha512-dZq5TuL/b7auLwX/gA/kEUpk2uNiMfYptHKsOPjxdOqaT45cXF5bYLovKMT7YokmYsLMnJAwfvNZHEgAA74WAw==","signatures":[{"sig":"MEYCIQCLZZDOpOIiGwHDSKkET0M2NE5GttpPurY3m/YJ6RGVLgIhAMBW0c5aX8PbsJVAuXjoY19ii9weLP5SvXjl9x/qRAbu","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91309},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"2180f9bce24f128f79f237efd4877adccba80a5f","scripts":{"lint":"eslint .","test":"vitest run","build":"yarn clean && tsc","clean":"rimraf dist","lint:fix":"eslint . --fix","test:watch":"vitest","test:coverage":"vitest run --coverage --coverage.include=\"src/**/*.ts\"","prepublishOnly":"yarn build"},"_npmUser":{"name":"kireban","email":"kka@3cx.com"},"_npmVersion":"11.16.0","description":"SDK for interacting with the 3CX Call Control API","directories":{},"_nodeVersion":"24.18.0","dependencies":{"ws":"^8.21.1","axios":"^1.18.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"yarn@4.17.1","devDependencies":{"eslint":"^10.8.0","rimraf":"^6.1.3","vitest":"^4.1.10","@types/ws":"^8.18.1","@eslint/js":"^10.0.1","typescript":"6.0.3","@types/node":"^24.13.3","typescript-eslint":"^8.65.0","@vitest/coverage-v8":"^4.1.10"},"peerDependencies":{"@types/node":">=24"},"peerDependenciesMeta":{"@types/node":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/call-control-sdk_0.1.8_1785326064351_0.48827920505578226","host":"s3://npm-registry-packages-npm-production"}},"0.1.9":{"name":"@3cx/call-control-sdk","version":"0.1.9","keywords":["3cx","pbx","call-control","voip","telephony","sdk","ai","voice-agent"],"author":{"name":"3CX"},"license":"MIT","_id":"@3cx/call-control-sdk@0.1.9","maintainers":[{"name":"sherlock1982","email":"nick.orekhov@gmail.com"},{"name":"kireban","email":"kka@3cx.com"}],"dist":{"shasum":"5c3c8719329f22d2f25e355272986710a4b3abd5","tarball":"https://registry.npmjs.org/@3cx/call-control-sdk/-/call-control-sdk-0.1.9.tgz","fileCount":33,"integrity":"sha512-ACIqRV9EicqGoDwkKB7EsHJP+UEqBIFfHXfE1vX9NeOVcgY2LxLGZR/JNw5lREbJ82QajE5VgFQT47tCcCEsSw==","signatures":[{"sig":"MEQCIHxq54JjuMONYvxPvFFYQ+WcyUfXMP38vBB0EI8nS4ibAiAGOtShSfB/FGxPL7/0ylC5ihZohSYofCU4E/ZOheWbWQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":92417},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=24.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"1326457183bb365225a2da5df526fba0577fd01d","scripts":{"lint":"eslint .","test":"vitest run","build":"yarn clean && tsc","clean":"rimraf dist","lint:fix":"eslint . --fix","test:watch":"vitest","test:coverage":"vitest run --coverage --coverage.include=\"src/**/*.ts\"","prepublishOnly":"yarn build"},"_npmUser":{"name":"kireban","email":"kka@3cx.com"},"_npmVersion":"11.16.0","description":"SDK for interacting with the 3CX Call Control API","directories":{},"_nodeVersion":"24.18.0","dependencies":{"ws":"^8.21.1","axios":"^1.18.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"yarn@4.17.1","devDependencies":{"eslint":"^10.8.0","rimraf":"^6.1.3","vitest":"^4.1.10","@types/ws":"^8.18.1","@eslint/js":"^10.0.1","typescript":"6.0.3","@types/node":"^24.13.3","typescript-eslint":"^8.65.0","@vitest/coverage-v8":"^4.1.10"},"peerDependencies":{"@types/node":">=24"},"peerDependenciesMeta":{"@types/node":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/call-control-sdk_0.1.9_1785403689088_0.2654060631516242","host":"s3://npm-registry-packages-npm-production"}},"0.1.10":{"name":"@3cx/call-control-sdk","version":"0.1.10","description":"SDK for interacting with the 3CX Call Control API","author":{"name":"3CX"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"yarn clean && tsc","clean":"rimraf dist","prepublishOnly":"yarn build","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage --coverage.include=\"src/**/*.ts\"","lint":"eslint .","lint:fix":"eslint . --fix"},"packageManager":"yarn@4.17.1","keywords":["3cx","pbx","call-control","voip","telephony","sdk","ai","voice-agent"],"license":"MIT","publishConfig":{"access":"public"},"engines":{"node":">=24.0.0"},"dependencies":{"axios":"^1.18.1","ws":"^8.21.1"},"peerDependencies":{"@types/node":">=24"},"peerDependenciesMeta":{"@types/node":{"optional":true}},"devDependencies":{"@eslint/js":"^10.0.1","@types/node":"^24.13.3","@types/ws":"^8.18.1","@vitest/coverage-v8":"^4.1.10","eslint":"^10.8.0","rimraf":"^6.1.3","typescript":"6.0.3","typescript-eslint":"^8.65.0","vitest":"^4.1.10"},"gitHead":"304c4e58dac04ab45d8477ddbeff57b5fffe0875","_id":"@3cx/call-control-sdk@0.1.10","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-UyaeKKre1yMy8exBp0SByIXDKC6ksWhIA0HkaBbogrcndbSjDwH9gkiXjep20CH+ux7fjQaYMGbwobK+myzR2w==","shasum":"460341f95e612a613b72b01460855f652971a27f","tarball":"https://registry.npmjs.org/@3cx/call-control-sdk/-/call-control-sdk-0.1.10.tgz","fileCount":33,"unpackedSize":97067,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCT2O3IeNhJpkjjb2jdPkRSXsoOgJ1SYQuE6upx7BIq0wIgFQ6MLyWVqJqbVp3nziiWQ6KnL346ZEDf0vh21jactk0="}]},"_npmUser":{"name":"kireban","email":"kka@3cx.com"},"directories":{},"maintainers":[{"name":"sherlock1982","email":"nick.orekhov@gmail.com"},{"name":"kireban","email":"kka@3cx.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/call-control-sdk_0.1.10_1785826711998_0.05116350659236102"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-29T09:06:09.131Z","modified":"2026-08-04T06:58:32.327Z","0.1.7":"2026-07-29T09:06:09.475Z","0.1.8":"2026-07-29T11:54:24.537Z","0.1.9":"2026-07-30T09:28:09.246Z","0.1.10":"2026-08-04T06:58:32.134Z"},"author":{"name":"3CX"},"license":"MIT","keywords":["3cx","pbx","call-control","voip","telephony","sdk","ai","voice-agent"],"description":"SDK for interacting with the 3CX Call Control API","maintainers":[{"name":"sherlock1982","email":"nick.orekhov@gmail.com"},{"name":"kireban","email":"kka@3cx.com"}],"readme":"# @3cx/call-control-sdk\r\n\r\nThe 3CX Call Control SDK for TypeScript provides a higher-level interface to the 3CX Call Control API.\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @3cx/call-control-sdk\r\n```\r\n\r\n## Quick Start\r\n\r\n```typescript\r\nimport { CallControlClient } from \"@3cx/call-control-sdk\";\r\n\r\nconst client = new CallControlClient({\r\n  pbxBase: \"https://your-pbx.example.com\",\r\n  appId: \"your-app-id\",\r\n  appSecret: \"your-app-secret\",\r\n});\r\n\r\nawait client.connect();\r\n\r\nclient.on(\"participantConnected\", async (participant) => {\r\n  console.log(`Participant ${participant.id} connected`);\r\n\r\n  // Get audio stream from the caller (PCM 8kHz 16-bit mono)\r\n  const audioStream = await participant.getAudioStream();\r\n\r\n  // Create audio writer to the caller\r\n  const audioWriter = participant.getAudioWriter();\r\n\r\n  // Pipe audio through your AI provider (STT -> Agent -> TTS)\r\n  // ...\r\n\r\n  // Write PCM 8kHz 16-bit mono audio back\r\n  audioWriter.write(pcmBuffer);\r\n\r\n  // Call actions directly on the participant\r\n  await participant.transfer(\"1001\");\r\n});\r\n\r\nclient.on(\"participantDisconnected\", (participantId) => {\r\n  // SDK automatically cleans up audio streams/writers\r\n  console.log(`Participant ${participantId} disconnected`);\r\n});\r\n```\r\n\r\n## WebSocket Reconnection\r\n\r\nOn unexpected disconnects the SDK reconnects automatically with exponential backoff.\r\nBy default it **never gives up** (`maxReconnects: Infinity`).\r\n\r\n| Option | Default | Description |\r\n| --- | --- | --- |\r\n| `websocket.maxReconnects` | `Infinity` | Attempts before giving up. Set a finite number to stop retrying. |\r\n| `websocket.reconnectDelayMs` | `5000` | Base delay before the first retry (doubles each attempt). |\r\n| `websocket.reconnectBackoffMaxMs` | `120000` | Upper cap per retry (2 minutes). |\r\n\r\nDelays grow roughly 5s → 10s → 20s → … up to the cap. Token fetch failures during\r\nreconnect use the same backoff. To limit retries:\r\n\r\n```typescript\r\nwebsocket: { maxReconnects: 5 }\r\n```\r\n\r\n## Extension Participant Monitoring\r\n\r\nWhen extensions are attached to the app's service principal (controlled DNs), the SDK\r\nemits separate events for their call activity. This lets you monitor calls on those\r\nextensions without conflating them with calls on the app's own programmable DN.\r\n\r\n```typescript\r\n// Calls on the app's own DN - full control (audio + call actions)\r\nclient.on(\"participantConnected\", async (participant) => {\r\n  console.log(`Own DN call from ${participant.info.party_caller_id}`);\r\n  const audio = await participant.getAudioStream(); // works\r\n  // ...\r\n});\r\n\r\n// Calls on monitored extensions - call actions only, no audio\r\nclient.on(\"extensionParticipantConnected\", async (participant) => {\r\n  console.log(\r\n    `Extension ${participant.dn} call from ${participant.info.party_caller_id}`,\r\n  );\r\n  // participant.getAudioStream() -> throws Error\r\n  await participant.transfer(\"1001\"); // works\r\n});\r\n\r\nclient.on(\"extensionParticipantDisconnected\", (participantId) => {\r\n  console.log(`Extension call ${participantId} ended`);\r\n});\r\n```\r\n\r\n### Why no audio on extension participants?\r\n\r\nAudio streams and DTMF are only available for the app's own programmable DN (RoutePoint).\r\nExtension calls are physically handled by the extension's own device (IP phone, softphone,\r\netc.) - the media path goes directly between the PBX and that device. The programmable\r\nextension never sits in the media path for those calls, so there is no audio to intercept\r\nor inject. Call control actions (transfer, drop, divert, routeTo) work at the signaling\r\nlevel; `answer()` on extensions requires `supportsDirectControl` (uaCSTA).\r\n\r\n### Event lifecycle note\r\n\r\nA RoutePoint (the app's own DN) answers incoming calls automatically - participants\r\nalmost always arrive with status `Connected`, so `participantConnected` fires immediately.\r\n\r\nExtensions behave differently: a call first rings on the device (`Ringing`), and only\r\nbecomes `Connected` after the user picks up. The `extensionParticipantConnected` event\r\nfires only when the extension actually answers the call. To detect incoming (ringing)\r\ncalls on extensions, listen to `extensionParticipantUpdated` and check\r\n`participant.info.status`:\r\n\r\n```typescript\r\nclient.on(\"extensionParticipantUpdated\", (participant) => {\r\n  if (participant.info.status === \"Ringing\") {\r\n    console.log(`Extension ${participant.dn} is ringing`);\r\n    if (participant.supportsDirectControl) {\r\n      void participant.answer(); // uaCSTA leg only\r\n    }\r\n  }\r\n});\r\n```\r\n\r\nMulti-device extensions get **one participant leg per device**. Call `answer()` on the\r\nleg with `supportsDirectControl === true`, not on mobile/softphone legs.\r\n\r\n| Capability      | Own DN (RoutePoint)                                 | Extension DNs                                                |\r\n| --------------- | --------------------------------------------------- | ------------------------------------------------------------ |\r\n| Call events     | `participantConnected` / `Updated` / `Disconnected` | `extensionParticipantConnected` / `Updated` / `Disconnected` |\r\n| Audio streams   | Yes                                                 | No                                                           |\r\n| DTMF            | Yes                                                 | No                                                           |\r\n| Transfer / Drop | Yes                                                 | Yes                                                          |\r\n| Divert          | Yes                                                 | Yes — **`Ringing` inbound only**                             |\r\n| RouteTo         | Yes                                                 | Yes — **`Ringing` or `Connected`**                           |\r\n| Answer          | Yes                                                 | Only with `supportsDirectControl` (uaCSTA)                   |\r\n| Attach data     | Yes                                                 | Yes                                                          |\r\n\r\n`divert()` redirects an unanswered call — use on a **`Ringing` inbound** leg (fails on\r\n`Connected`). `routeTo()` adds alternative routes while the participant stays in the call —\r\nvalid on **`Ringing` or `Connected`**. Use `transfer()` for blind transfer on established\r\ncalls. Neither applies to an outbound **`Dialing`** leg.\r\n\r\n## Outbound Calls\r\n\r\n`makeCall` returns the created participant ID directly from the API response, so you\r\ndon't have to wait for a WebSocket event to correlate the call:\r\n\r\n```typescript\r\nconst participantId = await client.makeCall(\"101\");\r\n// or from a specific source DN:\r\nconst participantId = await client.makeCall(\"101\", \"cctest2\");\r\n\r\nif (participantId !== undefined) {\r\n  await client.attachPartyData(participantId, { public_ticket: \"1234\" });\r\n}\r\n```\r\n\r\nThe PBX may return `202 Accepted` (call accepted, id not yet available). In that case\r\n`makeCall` resolves to `undefined` - track the call via WebSocket events instead.\r\n\r\n## Attached Data\r\n\r\nAttach metadata to a call with `attachParticipantData` / `attachPartyData`. **Every key\r\nmust be prefixed with `public_`** - the PBX rejects other keys with HTTP `422`.\r\n\r\nBoth methods take **the same participant ID** and write two different fields on **that\r\nparticipant's** record (the same call leg). They do **not** target the collocutor's DN\r\nor a second participant object:\r\n\r\n| SDK API / accessor                          | PBX field on this participant | Meaning                                                             |\r\n| ------------------------------------------- | ----------------------------- | ------------------------------------------------------------------- |\r\n| `attachParticipantData` / `participantData` | `participant_attached_data`   | Data on this participant's own leg                                  |\r\n| `attachPartyData` / `partyData`             | `caller_attached_data`        | Collocutor data in the party slot of this leg (same participant ID) |\r\n\r\nBoth values are read from the **controlled participant handle** — the collocutor is not\r\nexposed as a separate object. Attach party data on a monitored extension when you need\r\nmetadata to survive transfer or similar operations.\r\n\r\n```typescript\r\n// via the client (same participantId for both)\r\nawait client.attachParticipantData(participantId, { public_ticket: \"1234\" });\r\nawait client.attachPartyData(participantId, { public_peer: \"181\" });\r\n\r\n// or via a participant handle\r\nawait participant.attachParticipantData({ public_ticket: \"1234\" });\r\nawait participant.attachPartyData({ public_peer: \"181\" });\r\n\r\nconsole.log(participant.participantData.public_ticket); // '1234'\r\nconsole.log(participant.partyData.public_peer); // '181'\r\n\r\nawait participant.attachPartyData({ public_ticket: \"5678\" });\r\nawait participant.refresh(); // party data is not pushed over WebSocket\r\nconsole.log(participant.partyData.public_ticket); // '5678'\r\n```\r\n\r\nTo inspect another DN's legs, use `client.getState().callcontrol.get(dn)?.participants`\r\n(or the live handle from `getExtensionParticipantHandle`). There is no\r\n`getParticipantByDn` helper.\r\n\r\n## MCP Integration\r\n\r\nUse the SDK's token store with 3CX MCP server:\r\n\r\n```typescript\r\nconst authProvider = client.createMcpAuthProvider();\r\nconst mcpUrl = client.getMcpUrl();\r\n\r\nconst mcpServer = new MCPServerStreamableHttp({\r\n  url: mcpUrl,\r\n  authProvider,\r\n});\r\n```\r\n\r\n## API Reference\r\n\r\n### `Participant`\r\n\r\n| Method                                    | Description                                                                                         |\r\n| ----------------------------------------- | --------------------------------------------------------------------------------------------------- |\r\n| `id`                                      | Numeric participant ID                                                                              |\r\n| `dn`                                      | DN number this participant belongs to                                                               |\r\n| `isExtensionParticipant`                  | `true` if this is a monitored extension participant                                                 |\r\n| `info`                                    | Raw 3CX `CallParticipant` metadata                                                                  |\r\n| `destroyed`                               | Whether participant has been cleaned up                                                             |\r\n| `supportsDirectControl`                   | PBX `direct_control` (uaCSTA); required for `answer()` on extensions                                |\r\n| `participantData`                         | This leg's `participant_attached_data` via `attachParticipantData()` (`{}` if none)                 |\r\n| `partyData`                               | Collocutor's `caller_attached_data` on this leg via `attachPartyData()` (`{}` if none)              |\r\n| `getAudioStream()`                        | Get readable PCM audio stream (own DN only)                                                         |\r\n| `getAudioWriter()`                        | Get writable audio channel (own DN only)                                                            |\r\n| `makeCall(to)`                            | Place a new independent outbound call from this participant's DN                                    |\r\n| `transfer(destination, reason?)`          | Transfer to destination                                                                             |\r\n| `drop()`                                  | Drop from call                                                                                      |\r\n| `answer()`                                | Answer call (RoutePoint or `supportsDirectControl` only)                                            |\r\n| `routeTo(destination, timeout?, reason?)` | Add alternative routes (`Ringing` or `Connected`)                                                   |\r\n| `divert(destination, reason?)`            | Redirect unanswered call (`Ringing` inbound only)                                                   |\r\n| `transferToVoiceMail(dn, reason?)`        | Transfer to voicemail                                                                               |\r\n| `routeToVoiceMail(dn, timeout?, reason?)` | Route to voicemail                                                                                  |\r\n| `divertToVoiceMail(dn, reason?)`          | Divert to voicemail                                                                                 |\r\n| `attachParticipantData(data)`             | Write `participant_attached_data` on this leg                                                       |\r\n| `attachPartyData(data)`                   | Write `caller_attached_data` on this leg (not the remote DN)                                        |\r\n| `refresh()`                               | Fetch this leg from the REST API and update cached state                                            |\r\n| `cancelStreamQueue()`                     | Cancel PBX-queued outgoing audio (ends active POST; prefer `getAudioWriter().clear()` for barge-in) |\r\n\r\n### `CallControlClient`\r\n\r\n| Method                                             | Description                                                                                                                                                               |\r\n| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\r\n| `connect()`                                        | Connect to 3CX PBX (auth + WebSocket)                                                                                                                                     |\r\n| `disconnect()`                                     | Disconnect and clean up resources                                                                                                                                         |\r\n| `getParticipantHandle(id)`                         | Look up active **own-DN** `Participant` handle by ID                                                                                                                      |\r\n| `getExtensionParticipantHandle(id)`                | Look up active **extension** `Participant` handle by ID (use this for monitored DNs)                                                                                      |\r\n| `getAudioStream(participantId)`                    | Get readable PCM audio stream                                                                                                                                             |\r\n| `createAudioWriter(participantId)`                 | Create writable audio channel with keep-alive                                                                                                                             |\r\n| `transfer(participantId, destination)`             | Transfer participant to extension                                                                                                                                         |\r\n| `drop(participantId)`                              | Drop participant from call                                                                                                                                                |\r\n| `answer(participantId)`                            | Answer call (RoutePoint or `direct_control` / uaCSTA only)                                                                                                                |\r\n| `routeTo(participantId, destination)`              | Add alternative routes (`Ringing` or `Connected`). No `timeout` — use `participant.routeTo()` for timeout                                                                 |\r\n| `divert(participantId, destination)`               | Redirect unanswered call (`Ringing` inbound only)                                                                                                                         |\r\n| `makeCall(to, from?)`                              | Place outbound call. Returns the created participant ID (HTTP 200) or `undefined` (HTTP 202). `from` defaults to `appId`                                                  |\r\n| `attachParticipantData(participantId, data)`       | Write `participant_attached_data` on that participant's leg                                                                                                               |\r\n| `attachPartyData(participantId, data)`             | Write `caller_attached_data` on that participant's leg (same ID; not the remote DN)                                                                                       |\r\n| `cancelStreamQueue(participantId)`                 | Cancel PBX-queued outgoing audio (ends active POST; prefer `createAudioWriter().clear()` for barge-in)                                                                    |\r\n| `controlParticipant(participantId, method, body?)` | Generic call control action                                                                                                                                               |\r\n| `getParticipant(id)`                               | Raw `CallParticipant` from state for the **app's own DN only**. Extension legs: use `getExtensionParticipantHandle(id)` or `getState().callcontrol.get(dn)?.participants` |\r\n| `getState()`                                       | Get full call control state (all visible DNs and their participants)                                                                                                      |\r\n| `getFullInfo()`                                    | Fetch full state from REST API (does not update cache)                                                                                                                    |\r\n| `refreshParticipant(participantId)`                | Fetch one participant from REST API and update cached state + handles                                                                                                     |\r\n| `createMcpAuthProvider()`                          | Create MCP auth provider                                                                                                                                                  |\r\n| `getMcpUrl()`                                      | Get MCP endpoint URL                                                                                                                                                      |\r\n\r\n### Events - RoutePoint (Own DN)\r\n\r\nFired for participants on the app's own programmable DN. Full control: audio streams, DTMF, and call actions.\r\n\r\n| Event                     | Payload          | Description                                      |\r\n| ------------------------- | ---------------- | ------------------------------------------------ |\r\n| `participantConnected`    | `Participant`    | Participant connected to call                    |\r\n| `participantDisconnected` | `number`         | Participant removed from call                    |\r\n| `participantUpdated`      | `Participant`    | Participant state changed                        |\r\n| `dtmf`                    | `string, number` | DTMF digits received and Participant who pressed |\r\n\r\n### Events - Monitored Extensions\r\n\r\nFired for participants on extensions attached to the service principal. Call control only - no audio or DTMF (the media path is handled by the extension's own device, not by the programmable extension).\r\n\r\n| Event                              | Payload       | Description                         |\r\n| ---------------------------------- | ------------- | ----------------------------------- |\r\n| `extensionParticipantConnected`    | `Participant` | Extension participant connected     |\r\n| `extensionParticipantDisconnected` | `number`      | Extension participant removed       |\r\n| `extensionParticipantUpdated`      | `Participant` | Extension participant state changed |\r\n\r\n### Events - Connection\r\n\r\n| Event          | Payload | Description            |\r\n| -------------- | ------- | ---------------------- |\r\n| `connected`    | -       | WebSocket connected    |\r\n| `disconnected` | -       | WebSocket disconnected |\r\n| `error`        | `Error` | Error occurred         |\r\n\r\n## License\r\n\r\nMIT\r\n","readmeFilename":"README.md"}