{"_id":"@belsmart/crm-sdk","_rev":"10-9745eaed3c499f9b57796f34bd25fbec","name":"@belsmart/crm-sdk","dist-tags":{"latest":"0.11.0"},"versions":{"0.2.0":{"name":"@belsmart/crm-sdk","version":"0.2.0","license":"MIT","_id":"@belsmart/crm-sdk@0.2.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"21080e4d4691a82b7df51272860a7a597f131c65","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.2.0.tgz","fileCount":8,"integrity":"sha512-V9L06ketXcCH4ZB4l3036ukeZxBWM7otkkc/MH5W1OvcznsAencH2Ql7AZL2/L6gqEcI5Gdn/JUZmAPbcn/mwg==","signatures":[{"sig":"MEYCIQDqhf8bIrxo4NnACRSvIw8b8zu+ehZET6bxpKk80nTjJAIhAMSSC7jXDtNNfh/8iq1r4gLCug2bjEiPux6erHM9PvFS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":588369},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with BelSmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.2.0_1787737720241_0.33924743351577047","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@belsmart/crm-sdk","version":"0.3.0","license":"MIT","_id":"@belsmart/crm-sdk@0.3.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"11e2d990479919891c112e07071ab1c52b5bfa8b","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.3.0.tgz","fileCount":8,"integrity":"sha512-PsBKwy8ezH9qJCeaXkbjLO+smzQHcZ1j9RZvX+LFDdgFBkl0Cp+55wMzleAlJ5OGwE6qYgDGn+4tLBvU9M56fw==","signatures":[{"sig":"MEYCIQC4JTttfmnHy0eRCPZgLPSaAFVTCEIpeYfZCb9mQKZ+6AIhAMmh41gKX7UAwFaGcS/plt8Z5JUzy1vSRm6Bv/wCukDa","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":594081},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with BelSmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.3.0_1787739113285_0.5143559595712968","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@belsmart/crm-sdk","version":"0.4.0","license":"MIT","_id":"@belsmart/crm-sdk@0.4.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"4f5c313b34265ad0fd4cf6cfe2e32b525d833cf5","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.4.0.tgz","fileCount":8,"integrity":"sha512-TrrgvIoC/dxUnQN+R/DLTtRsyJpdDgNXGJ9H6U4CjPLwZ40FPoKrTmr0lgNmjo36WFkejkRA+WyP87qjCqWi8A==","signatures":[{"sig":"MEQCIGs9kN97FB4LB7HZjXjTG3tZWBdNjV54NpkSnxzEl1OmAiA6WKz0zD0nL+NjDn+H2kX+7ruJYjy6ArGrRNwrQCq14A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":625902},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with BelSmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.4.0_1787750038396_0.7618468214887044","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@belsmart/crm-sdk","version":"0.5.0","license":"MIT","_id":"@belsmart/crm-sdk@0.5.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"c4c07828c46bb50a19125017aa78d96417dcb9f1","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.5.0.tgz","fileCount":8,"integrity":"sha512-Q/N5FgC3OPaWWX1QZohkf0nMh2JSuLPyzos0rQih9EP3wgqszdusPwDySPigyNenWndbmUVzOBDb0stE/IDnwg==","signatures":[{"sig":"MEYCIQDxWrhAu3D4YqlCqscyzEGVEFn6klOc77qMUd/UCApLRQIhAMVsUtsVcl2oOeibdSjZ8iDbCbH6nPwCVZyAY1zaLbor","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":748912},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.5.0_1787836639860_0.3398932606919891","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@belsmart/crm-sdk","version":"0.6.0","license":"MIT","_id":"@belsmart/crm-sdk@0.6.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"8908126f6ddc5c0476ec91d41faa0cdc6a5fc447","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.6.0.tgz","fileCount":8,"integrity":"sha512-QnOzJ4f3px933bKp4iEwq+3SkYA8LSDmAiiswhK9qyOFtbj6v2osA7DU48I2A9yIGDrSRvGFfzZ0ADCD8bmX8w==","signatures":[{"sig":"MEUCIGpJyJlitGjQUJUqVi8yGtPWY/evZLkcTVjVTTB6fIiuAiEA4ghUW6S9AHqp+20kE4CBj/2rs19D7t1DAwuco0nlwpM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":772657},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.6.0_1787913726606_0.6648877253049708","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@belsmart/crm-sdk","version":"0.7.0","license":"MIT","_id":"@belsmart/crm-sdk@0.7.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"bda83e9054011901595215f0f6001e1328859201","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.7.0.tgz","fileCount":8,"integrity":"sha512-+E6v7rBYdPMhqPt/j19t45peT0J+JjKoXfpZh4Ujk5/UPrWIjLsiMnSXK1cSkZdqji0HyVQRSWv5ByNEi81VyQ==","signatures":[{"sig":"MEUCIBSGmIDVoXujvZNezcfQZfxbHAr97JG2VX5yuwg+5FRxAiEAtwN8gt/7oiFV31yDYpqS6zvtUmx/DhzP2JvbOmjkXvU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQD3iM4Yybfb2QkCiNm/q67I0V9UvCrHnLwRqwmXupETgQIhAMi60JXwDGO/IEUuBbiFjGeEghD7VKyQbEj/3fTviDN2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":785085},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","gitHead":"0361cc65dbf6a5065ea8240f9c8bdbf67b7c8a0e","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.7.0_1789037229604_0.07776043182982995","host":"s3://npm-registry-packages-npm-production"}},"0.8.1":{"name":"@belsmart/crm-sdk","version":"0.8.1","license":"MIT","_id":"@belsmart/crm-sdk@0.8.1","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"39c821a596f7e7949d6a34ea4f270df4120201aa","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.8.1.tgz","fileCount":8,"integrity":"sha512-9u6o2KdeRVEGrqzwURKt1TAguuci30FgnicaS/MO5gG14VbjZhLBk4suJnhvk+nXNcWbPmQCXrTHO+0Eo0TG+g==","signatures":[{"sig":"MEUCIQC5UM04afF7vTiVAwPI3rTseZycPnULGcK/U5l9bPZF6AIgVNNfJyNcf32aPb3bDFR7ZaVxWYKc5xPa/bR+QGAt0MY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIDO0rwNibIxsn0TUjTfXwxWcSSC67md28PwsGxWGn0PfAiBgMKGdiaqnADmiRbcCFYSYbBeqzeTxZB7wSXZT5bRDHA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":811169},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","gitHead":"c6ce4703bd40ce09001ef457fd40765630a78977","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.8.1_1789555556125_0.3851631271903946","host":"s3://npm-registry-packages-npm-production"}},"0.9.0":{"name":"@belsmart/crm-sdk","version":"0.9.0","license":"MIT","_id":"@belsmart/crm-sdk@0.9.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"e05f1a0797e1964899b91189c68b7a11e0c0de21","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.9.0.tgz","fileCount":8,"integrity":"sha512-QNnERBD00Zg8e/gw46dRa4zxvr5JUSIphweaWVKLjSFv4TPC/3l86UbglkKmeRfOYfUXvAX9BCQDfBmiViDNzA==","signatures":[{"sig":"MEQCIF7Jkz5x6dRGqRjmR2wVRbmnwkMrIL4OkX5LQ1bNBbiYAiAV3WB8mOf1aJmuHDdRgMO9lNEX3WstZMrTy1FEPPciZA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCID9P4oNok/AE0BCWNGbXnYPbPSCN7wP539q1d86pqCBOAiEAwb+fwxSFx4duPsOafQf3/3dH/r2R8N9wpL8eqvZ9w0Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":833361},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","gitHead":"00d69c28bf94a1217a6186349c092556952ae860","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.9.0_1789983311414_0.5320477156385712","host":"s3://npm-registry-packages-npm-production"}},"0.10.0":{"name":"@belsmart/crm-sdk","version":"0.10.0","license":"MIT","_id":"@belsmart/crm-sdk@0.10.0","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"dist":{"shasum":"576d63f27f7b3e4713101d204de47ded68947c58","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.10.0.tgz","fileCount":8,"integrity":"sha512-CxjuQo7SdCvHDCylXqavPs5YniMM5X+NtWXUeo4KIirpXpebWYO4WeohOUIc/B3afyairHn7MPHG8OML3/dn7A==","signatures":[{"sig":"MEUCIQCUl2zTRvmIBELEXNigMe1eZ76ak7VKh7t/T00YD/GR4QIgXq4P8Z5pCdwnnfOTpHEKNph5pyXaRBUzarEhKNx362I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIES1htkjskagFTNPvTvcjY3ZmnayIXVk1Dk7/RmDeCO2AiEAr/Ip+fgQwvL27gXFtajzmV+w/gZQ3utX+1MLD+/0fec=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1035948},"main":"dist/index.js","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","gitHead":"4a139102147f191dfd502f4c03fc8977c471bca4","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"tmp":"tmp/crm-sdk_0.10.0_1790246581742_0.31717664753164065","host":"s3://npm-registry-packages-npm-production"}},"0.11.0":{"_id":"@belsmart/crm-sdk@0.11.0","dist":{"shasum":"da6ad4cbff128da94d1265067e4d50c070a879d4","tarball":"https://registry.npmjs.org/@belsmart/crm-sdk/-/crm-sdk-0.11.0.tgz","fileCount":8,"integrity":"sha512-ose3ObYN9sXx4LK56iCCW7DeHbUUAvkwKFHzHDOHY300g3+QXDUFmmt24nyPvmvcgEmhLpg0Mu5ksvV0OAzgKw==","signatures":[{"sig":"MEUCIQClutMAmWURcIfvzXUA/87NxQwCi2lLp8p4zX6qUJRXYAIgFIWp8CYwmPWx014IRUMJfWPWbkCdkic/QjzKS9zBhdU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIA/UFuU49IjjsKUHtTzicVi3dDg5i/LvXsSEHqwOXfjeAiA0u0/AN8pEFWUcP74cFAkhRytVW7v1HPNpOTzpUjzrjg=="}],"unpackedSize":1089701},"main":"dist/index.js","name":"@belsmart/crm-sdk","types":"dist/index.d.ts","unpkg":"dist/index.global.js","module":"dist/index.mjs","gitHead":"801bbc5d826282852006161e8003cfb62afe91de","license":"MIT","scripts":{"dev":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --watch --out-dir dist","build":"tsup src/index.ts --format esm,cjs,iife --global-name BelSmartCRM --dts --clean --out-dir dist","typecheck":"tsc --noEmit"},"version":"0.11.0","_npmUser":{"name":"beltalk","email":"samim@beltalk.net"},"_npmVersion":"10.8.2","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","directories":{},"maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"_nodeVersion":"20.19.6","dependencies":{"sip.js":"^0.21.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/crm-sdk_0.11.0_1790756278490_0.0019973299871127725"}}},"time":{"created":"2026-08-26T09:48:40.078Z","modified":"2026-09-30T08:17:58.776Z","0.2.0":"2026-08-26T09:48:40.421Z","0.3.0":"2026-08-26T10:11:53.511Z","0.4.0":"2026-08-26T13:13:58.556Z","0.5.0":"2026-08-27T13:17:20.008Z","0.6.0":"2026-08-28T10:42:06.836Z","0.7.0":"2026-09-10T10:47:09.697Z","0.8.1":"2026-09-16T10:45:56.203Z","0.9.0":"2026-09-21T09:35:11.497Z","0.10.0":"2026-09-24T10:43:01.832Z","0.11.0":"2026-09-30T08:17:58.612Z"},"license":"MIT","description":"Click-to-call SDK and floating softphone widget for third-party CRM integrations with Belsmart.","maintainers":[{"name":"beltalk","email":"samim@beltalk.net"}],"readme":"# @belsmart/crm-sdk\n\nClick-to-call SDK and floating softphone widget for embedding Belsmart calling into a third-party CRM.\n\n**New to this SDK, or integrated before `0.8.0`?** Start with [`docs/INTEGRATION_GUIDE.md`](docs/INTEGRATION_GUIDE.md) — a current, end-to-end walkthrough of the recommended setup. This README is the narrative reference; [`docs/API.md`](docs/API.md) is the full method-by-method reference.\n\n## How it fits together\n\n- The **Belsmart gateway** (`partner-api` module in the main API) issues each CRM integration an API key/secret pair, scoped to one tenant.\n- That key/secret gets exchanged for a short-lived (12h) session token + SIP credentials via `POST /partner-api/v1/session`. The SDK can do this exchange itself (`BelsmartCrmConfig`, below) — simplest to wire up, but it means `apiSecret` has to be constructed with by browser code, which makes it readable from dev tools by any signed-in agent. **We recommend instead having your own backend perform this exchange** (`BelsmartCrmSessionConfig`) and handing the browser only the resulting session — see \"Keeping `apiSecret` off the browser\" below.\n- Either way, the SDK then registers a WebRTC softphone for the mapped agent using that session and exposes `call()` / `hangup()` / events.\n- The **widget** is an optional floating UI built on top of the SDK — a dial pad, active-call bar, and incoming-call popup, rendered inside a Shadow DOM so it never inherits the host CRM's styles.\n\nClick-to-call works by ringing the agent's own SIP device first; once the SDK's softphone answers that leg, the Belsmart PBX bridges it to the destination number — the same mechanism the internal agent portal and HubSpot integration use.\n\n## Setup (one-time, done by the tenant admin)\n\n1. In the Belsmart admin, create a partner API key: `POST /partner-api/keys { \"name\": \"Acme CRM\" }`. The response's `key_secret` is shown once — store it securely.\n2. Map each CRM user to a Belsmart agent (`agent_uuid`) — store that mapping on the CRM side. Use `BelsmartCrmClient.listAgents()` (below) to fetch the options for that mapping UI instead of hardcoding UUIDs.\n\n## Usage\n\nMap a CRM user to an agent (once, e.g. in a settings screen), then construct the client:\n\n```ts\nimport { BelsmartCrmClient } from \"@belsmart/crm-sdk\";\n\n// No agentUuid needed yet — just the key pair.\nconst agents = await BelsmartCrmClient.listAgents({ apiBaseUrl, apiKey, apiSecret });\n// agents: [{ agentUuid, name, email, extension }, ...]\n// Show these in your own UI, let the CRM user pick one, store their choice\n// (e.g. as a custom field on your CRM user record) — then:\n\nconst client = new BelsmartCrmClient({ apiBaseUrl, apiKey, apiSecret, agentUuid: picked.agentUuid });\n```\n\n```html\n<!-- Pinned to a specific version — bump deliberately, don't float on latest. -->\n<script src=\"https://unpkg.com/@belsmart/crm-sdk@0.8.1/dist/index.global.js\"></script>\n<script>\n  // `session` comes from YOUR OWN backend, which already did the apiKey/apiSecret\n  // exchange server-side (see \"Keeping apiSecret off the browser\" below) — fetch\n  // it from your own endpoint however your CRM's frontend normally talks to its\n  // backend. apiKey/apiSecret never appear in this page.\n  fetch(\"/api/belsmart-session\", { credentials: \"include\" })\n    .then((res) => res.json())\n    .then((session) => {\n      const client = new BelSmartCRM.BelsmartCrmClient({\n        apiBaseUrl: \"https://pbx.example.com\",\n        agentUuid: \"<belsmart-agent-uuid-for-this-crm-user>\",\n        session,\n      });\n\n      client.on(\"incoming\", (call) => { /* show your own UI, or let the widget handle it */ });\n      client.on(\"active\", (call) => console.log(\"call active\", call));\n      client.on(\"ended\", (call) => console.log(\"call ended\", call, `${call.duration}s`));\n      // Fires for things like a denied/missing microphone (SDK 0.8.1+) — the\n      // call is also reported \"ended\" in that case, not left hanging.\n      client.on(\"error\", (err) => console.error(\"BelSmart CRM error:\", err.message));\n\n      client.init().then(() => {\n        BelSmartCRM.createWidget(client);\n      });\n    });\n</script>\n```\n\nSimplest-to-wire-up alternative, if you're prototyping and don't yet have a backend endpoint for the session exchange — **not recommended for production**, since `apiSecret` ends up readable from this page's own source/dev tools:\n\n```html\n<script src=\"https://unpkg.com/@belsmart/crm-sdk@0.8.1/dist/index.global.js\"></script>\n<script>\n  const client = new BelSmartCRM.BelsmartCrmClient({\n    apiBaseUrl: \"https://pbx.example.com\",\n    apiKey: \"pk_...\",\n    apiSecret: \"...\",\n    agentUuid: \"<belsmart-agent-uuid-for-this-crm-user>\",\n  });\n\n  client.on(\"error\", (err) => console.error(\"BelSmart CRM error:\", err.message));\n\n  client.init().then(() => {\n    BelSmartCRM.createWidget(client);\n  });\n</script>\n```\n\nOr via npm, with your own UI instead of the built-in widget:\n\n```ts\nimport { BelsmartCrmClient } from \"@belsmart/crm-sdk\";\n\nconst client = new BelsmartCrmClient({ apiBaseUrl, apiKey, apiSecret, agentUuid });\nawait client.init();\n\ndocument.querySelector(\"#call-btn\")?.addEventListener(\"click\", () => {\n  // Country code + national number, digits only — no leading \"+\", no separators.\n  client.call(\"15551234567\", \"crm-contact-42\");\n});\n```\n\n## Keeping `apiSecret` off the browser (recommended)\n\n`apiKey`/`apiSecret` are tenant-wide credentials meant to live server-side. The only thing they're ever used for is the one-time exchange at `POST /partner-api/v1/session` — every call-control operation afterwards (`call()`, `hangup()`, conference/consult, CDR, recording) runs off the short-lived `sessionToken` that exchange returns. So instead of constructing `BelsmartCrmClient` with the raw key pair in browser code, have your **own backend** perform that exchange and hand the browser only the resulting session:\n\n```ts\n// --- your backend (holds apiKey/apiSecret) ---\nconst res = await fetch(`${apiBaseUrl}/partner-api/v1/session`, {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"X-Api-Key\": apiKey,\n    \"X-Api-Secret\": apiSecret,\n  },\n  body: JSON.stringify({ agent_uuid: pickedAgentUuid }),\n});\nconst { data } = await res.json();\n// Hand `data` down to the browser over your own authenticated session/API —\n// map it to SessionInfo's camelCase shape (sessionToken, expiresIn, tenantUuid,\n// agentUuid, sip, wssUrl, iceServers, defaultCountryCode) the same way\n// ApiClient.createSession() does internally.\n```\n\n```ts\n// --- browser (never sees apiKey/apiSecret) ---\nimport { BelsmartCrmClient } from \"@belsmart/crm-sdk\";\n\nconst client = new BelsmartCrmClient({\n  apiBaseUrl,\n  agentUuid: pickedAgentUuid,\n  session, // the SessionInfo your backend fetched above\n});\nawait client.init(); // adopts `session` directly — no session-mint call, no secret involved\n```\n\n`BelsmartCrmClient.listAgents()`/`listDispositions()` also take the raw key pair (they're just thin `GET` wrappers) — call these from your backend too, and pass down only the resulting `AgentInfo[]`/`DispositionInfo[]` arrays for your agent/disposition-mapping UI.\n\n## API\n\nFull function-level reference: [`docs/API.md`](docs/API.md).\n\n### Setup / session\n\n- `BelsmartCrmClient.listAgents({ apiBaseUrl, apiKey, apiSecret })` — static; lists active agents for the tenant (`agentUuid`, `name`, `email`, `extension`) so the CRM can build its own agent-mapping UI. Only needs the key pair — call this before constructing a client. Prefer calling this from your backend (see \"Keeping `apiSecret` off the browser\") and passing just the result down.\n- `BelsmartCrmClient.listDispositions({ apiBaseUrl, apiKey, apiSecret }, agentUuid?)` — static; lists dispositions for the campaign bound to the key (or, with `agentUuid`, the campaign that agent picked via `setCampaign()`) (`dispositionUuid`, `name`, `code`), for building your own outcome-mapping UI. Returns `[]` (not an error) if the key has no campaign bound. Same backend-first recommendation as `listAgents()`.\n- `new BelsmartCrmClient(config)` — `config` is either:\n  - `BelsmartCrmConfig`: `apiBaseUrl`, `apiKey`, `apiSecret`, `agentUuid`, `debug?` — the SDK exchanges the key pair for a session itself in `init()`. Simplest, but puts `apiSecret` in browser code.\n  - `BelsmartCrmSessionConfig`: `apiBaseUrl`, `agentUuid`, `session` (a `SessionInfo` your backend already fetched from `POST /partner-api/v1/session`), `debug?` — `init()` adopts `session` directly, no secret involved. **Recommended** — see \"Keeping `apiSecret` off the browser\" above.\n  - `debug?` (both forms, default `false`) enables the SDK's own verbose internal logging for session/mute/hold/conference internals; `sip.js`'s own wire-level SIP logging is always kept suppressed regardless of this flag.\n- `client.init()` — authenticate and register the softphone. Call once before anything else.\n- `client.destroy()` — unregisters and tears down the transport.\n- `client.getAgentLabel()` — the signed-in agent's own SIP extension username (e.g. `\"Agent16\"`), or `null` before `init()`/after `destroy()`. Used by the built-in widget to label the agent's own row in a conference roster.\n- `client.getDefaultCountryCode()` — the country code configured on the agent's current campaign (see `setCampaign()`; the key's campaign by default), e.g. `\"1\"`, or `\"\"` if none is set. Purely informational — `call()` never reads it; it's there so your own custom UI can pre-fill a country-code picker the same way the built-in widget's dial pad does.\n\n### Campaign\n\nThe API key's campaign is the default. An agent assigned to more than one campaign (their user group in the tenant portal — active outbound campaigns only) can switch: calls, caller ID, outgoing rules, DNC, dispositions, the dial-pad country code and their live-dashboard presence then run under the picked campaign. The choice is remembered server-side for that agent + key, so it survives session refresh and the next sign-in.\n\n- `client.listCampaigns()` — `{ campaigns: CampaignInfo[], activeCampaignUuid, defaultCampaignUuid }`.\n- `client.getCampaign()` — the current `CampaignInfo` (`campaignUuid`, `name`, `allowCustomCallerId`), or `null` if the key has none bound.\n- `client.isCustomCallerIdAllowed()` — whether the current campaign lets the agent use their own caller ID (set per campaign by the master admin, \"Allow Custom Caller ID\").\n- `client.setCampaign(campaignUuid)` — switch; resolves with the new `CampaignInfo`, updates `getDefaultCountryCode()` and fires `\"campaign_changed\"`. Rejects with `BelsmartApiError` 409 during a call, 403 if the campaign isn't assigned to the agent or is inactive. In the built-in widget the campaign picker is under the settings (gear) icon in the header, shown while idle.\n\n### Pause\n\n- `client.setStatus(\"pause\" | \"idle\")` — pause the agent (live dashboard shows **Pause**, no queue calls; the agent can still call out) or resume to Idle. Kept across session refresh and sign-in until resumed or signed off. Fires `\"status_changed\"`. Rejects with `BelsmartApiError` 409 during a call.\n- `client.getStatus()` — `\"pause\"` or `\"idle\"`.\n- The built-in widget shows a Pause / Resume button above the dial pad and a \"Paused\" badge in its header.\n\n### Custom caller ID\n\nOn campaigns the master admin allowed, the agent can choose the caller ID the customer sees: `client.call(number, { callerId: \"14155550123\" })` (7-15 digits, optional leading `+`). On any other campaign the gateway rejects the call with `BelsmartApiError` 403. Checked on every call, so revoking takes effect immediately. The built-in widget shows an optional \"Your caller ID\" field on the dial pad only when `isCustomCallerIdAllowed()` is true, and remembers the last value in that browser. Transfers and conference keep their usual caller-ID rules.\n\n### Calls\n\n- `client.call(toNumber, externalRef?, leadName?)` — click-to-call; `externalRef` is echoed back untouched (e.g. a CRM contact id), `leadName` is display-only (shown by the built-in widget in place of the raw number). Rejects immediately if not currently connected (see \"Connection resilience\" below) or if a call is already in progress.\n- `client.hangup()` — ends an active call, or cancels one still waiting on its bridge leg. In a conference, this only drops **your own** leg — see `hangupAll()`.\n- `client.setMuted(bool)`, `client.isMuted()` — instant, client-side (no re-INVITE).\n- `client.hold()`, `client.unhold()`, `client.isOnHold()` — re-INVITEs with a held SDP so FreeSWITCH plays hold music to the far end; also mutes local playback.\n- `client.setVolume(0..1)`, `client.getVolume()` — local playback volume of the remote party only.\n- `client.setSoundsEnabled(bool)`, `client.isSoundsEnabled()` — short local beeps on connect/disconnect, mute/unmute and hold/unhold (agent's speakers only, never sent to the far end). On by default; set `sounds: false` in the config to start with them off.\n- `client.getConnectionStatus()` (`\"disconnected\" | \"connecting\" | \"connected\"`), `client.isConnected()` — tracked continuously, including reconnects after a WSS drop, not just the first `init()`.\n- `client.sendDtmf(digit)`\n- `client.getCurrentCall()` — the in-progress call, if any.\n\n### Conference (add a party)\n\n- `client.addToConference(toNumber, leadName?)` — adds a third party to the current call; from this point on, everyone (you, the customer, and every added party) can hear each other. `leadName` is display-only, shown by the built-in widget's roster in place of the raw number — same convention as `call()`'s. Sets `CallInfo.inConference = true`.\n- `client.hangupAll()` — ends the call for **every** party on it, not just your own leg. Only valid once `inConference` is `true`.\n- `client.listConferenceParticipants()` — `Promise<ConferenceParticipant[]>`; everyone currently on the call (you included), for building a roster with per-person controls. Returns `[]` if not currently in a conference. There's no push update for membership changes — call this again to refresh. The built-in widget does this automatically: after every action, briefly (every 2s, up to 60s) after adding a party until they actually answer, and continuously (every 5s) for as long as the call stays a conference, so someone hanging up on their own also disappears from the roster without a manual refresh.\n- `client.muteConferenceParticipant(memberId)` / `unmuteConferenceParticipant(memberId)` — mute/unmute one participant (server-side, via FreeSWITCH's conference member commands — not a local WebRTC track toggle, unlike `setMuted()`).\n- `client.holdConferenceParticipant(memberId)` / `unholdConferenceParticipant(memberId)` — hold/unhold one participant; they can't hear or be heard while held, everyone else is unaffected.\n- `client.kickConferenceParticipant(memberId)` — removes one participant; everyone else stays connected. `memberId` comes from `listConferenceParticipants()` — it's a FreeSWITCH conference member id, not a channel UUID.\n\n### Park & grab\n\nParked calls are shared across the whole tenant — with the Chrome extension and agent portal too — so any agent can grab any parked call. They don't time out, and there are no push updates: poll the list.\n\n- `client.park()` — `Promise<{ parkedCallId }>`; parks the current call. The gateway drops your leg and closes the call record itself, so no `/end` is sent. `\"parked\"` fires, then `\"ended\"` with `parked: true`. Not available on a conference (400).\n- `client.listParkedCalls()` — `Promise<ParkedCall[]>`; everything currently parked in the tenant. The built-in widget polls it every 5s while its \"Parked calls\" list is open.\n- `client.grabParkedCall(parked)` — `Promise<CallInfo>`; takes a parked call. The gateway rings you and the SDK answers that ring itself, so the call goes straight to `\"active\"` (caller shown from the park list, not FreeSWITCH's placeholder). Resolves once connected, with the call's new `callId` — hold, transfer, `/end`, heartbeats and CDR all use it from then on. Can take up to ~30s. Only while idle. Rejects with the gateway's message as-is: 409 already grabbed by someone else / not answered (back in the list), 404 caller hung up / no extension for this agent.\n- `client.mergeParkedCall(parked)` — `Promise<void>`; pulls any parked call into the current call. It becomes a conference, as with `addToConference()`, and the parked customer joins with no ring. Rejects with the gateway's message as-is: 409 already taken, 404 caller hung up.\n\n### Disposition\n\n- `client.setDisposition(disposition?, callId?)` — records an outcome for the most recently ended call (or a specific `callId`). Safe to call any time after a call ends. Not yet reflected in Belsmart's own CDR/reporting — purely for your own tracking today; wiring it into Belsmart's own reporting is coming soon.\n\n### CDR & recording\n\n- `client.getCallCdr(callId?)` — `Promise<CallCdr>`; the gateway's own call detail record (direction, numbers, start/answer/end timestamps, duration, talk-time, hangup cause, disposition). Defaults to the most recently ended call, same convention as `setDisposition()`.\n- `client.getCallRecording(callId?)` — `Promise<CallRecording | null>`; a short-lived signed playback/download URL (expires in ~10 minutes), or `null` if the call wasn't recorded or the gateway hasn't finished processing it yet — retry shortly after if you get `null` right after the call ends. Fetch/play or copy the file promptly — the URL isn't meant to be stored for later; call `getCallRecording()` again to mint a fresh one. Same `callId` convention as above.\n\n### Events & widget\n\n- `client.on(event, handler)` / `client.off(event, handler)` — `registered | unregistered | connection_status | incoming | connecting | ringing | active | consulting | ended | campaign_changed | mute | hold | error`. `on()` returns an unsubscribe function.\n- `createWidget(client, options?)` — mounts the floating dialer (dialpad with a country-code selector, mute/hold/keypad/conference controls, live SIP status dot, draggable by its header); returns `{ destroy() }`. The dial pad's country-code selector pre-fills from `getDefaultCountryCode()` but is always editable — the agent types the local number and the widget composes the full number itself before calling `call()`. `addToConference()`/`startConsult()`'s own number panels have no selector — the agent types the full number directly there, same as `call()`'s own contract.\n\n## Call lifecycle\n\n`CallInfo.state` moves through `\"connecting\" → \"ringing\" → \"active\" → \"ended\"` (or `\"consulting\"` in between, during an attended transfer):\n\n- **`connecting`** — `call()` has originated the click-to-call and is waiting for the bridge-leg INVITE to reach your own device.\n- **`ringing`** — your own leg is up (the bridge-leg INVITE was accepted), and Belsmart has started dialing the **customer's** number. This is *not* the same moment as `connecting` resolving — your leg connecting to Belsmart happens before the customer's phone ever rings. The SDK polls the gateway (~400ms intervals, up to 90s) to find out the moment the customer actually answers, since your browser's own SIP session has no visibility into that second leg at all.\n- **`active`** — the customer answered. This is also when `startedAt` is (re)stamped and the duration clock genuinely starts — not at `connecting`, and not merely at your own leg connecting.\n- **`consulting`** — mid-attended-transfer (`startConsult()` was called); the customer is held while you talk privately to a consult target.\n- **`ended`** — self-explanatory. `duration` reflects actual talk time (customer-answer to end), not total elapsed time since `call()`.\n\nIf you're building a custom UI, listen for `ringing` and `active` separately rather than treating `ringing` as already-connected — a duration timer or \"on call\" indicator should start at `active`, not `ringing`.\n\n## Connection resilience\n\n- **Registration refresh**: handled by `sip.js`'s own Registerer before the negotiated `expires` interval lapses — no extra polling on top of that.\n- **Unexpected unregistration**: if the registration itself drops to `Unregistered` (without the transport disconnecting), the SDK retries registering up to 5 times, 3 seconds apart, before giving up.\n- **Transport drop**: sip.js's own built-in transport reconnect runs first (a handful of attempts a few seconds apart); on top of that, the SDK runs its own backstop — a watchdog that keeps retrying every 10s for as long as it should be connected, plus an immediate retry on the browser's `online` event (covers a laptop sleep/wake or an outage longer than sip.js's own attempts). Either way, once reconnected the SDK re-registers automatically if it isn't already registered.\n- **Stuck bridge leg**: if `call()` originates but the bridge-leg INVITE never arrives within ~15s (e.g. the extension wasn't actually reachable when originated), the call auto-resolves to `ended` instead of sitting on `\"connecting\"` forever. `hangup()` also works on a call in this state, ending it immediately rather than waiting out the timeout.\n- **What's deliberately not covered**: a registration removed directly server-side (e.g. an admin flushing the registrar cache) sends no SIP message to the client at all, so nothing above can detect it until the next natural refresh or an actual failed registration attempt — this is a genuinely silent case, not something the watchdog above can help with (it reacts to a signaled disconnect; this has none). Closing it properly would need a signal outside SIP itself (e.g. your backend checking the registrar via ESL).\n- **Already-established calls are left alone**: a transient disconnect never force-ends a call that's already bridged — only a call still waiting on its bridge leg is treated as recoverable. An established call's media is independent of the signaling transport once negotiated, so a signaling blip doesn't mean the call died.\n- **Agent tab/browser closing mid-call**: `pagehide` fires the same way whether the agent closes just the tab, closes the whole browser, or navigates away — the SDK reacts to it with two independent best-effort signals: a SIP `BYE` over the still-open WS, and a `keepalive: true` call to `/calls/:id/end` (the browser explicitly keeps trying this one even after the page is gone, so it's the more reliable of the two). This usually ends the customer's leg on a deliberate close. It does **not** cover a browser crash, force-quit, or sudden loss of network — `pagehide` never fires for those, and the customer's leg is left connected with no agent on it until the gateway's own transport/RTP timeout (if any) clears it. See \"Known limitations\" below.\n\n## ICE servers (NAT traversal)\n\n`SessionInfo.iceServers` comes from the gateway's `POST /partner-api/v1/session` response (`ice_servers`) and is applied automatically when the SDK builds its `UserAgent` — no config needed on the SDK side. The gateway defaults to a public STUN server; set `PARTNER_STUN_URLS` there to override, and add TURN once available for agents behind symmetric NAT/strict firewalls (STUN alone won't help them).\n\n## Known limitations\n\n- The widget's \"incoming call\" UI doesn't yet surface the caller's number once answered as an active call — cosmetic only, the call itself works.\n- No TURN relay yet — only STUN. Agents behind symmetric NAT or strict corporate firewalls may still fail to establish the WebRTC leg.\n- Hold is implemented via hand-rolled SDP direction mangling — whether the far end actually hears hold music depends on the FreeSWITCH channel's hold-music configuration for that domain/extension.\n- A registration removed directly server-side (bypassing SIP signaling entirely) isn't detected until the next natural refresh — see \"Connection resilience\" above.\n- `setDisposition()` records an outcome on Belsmart's partner-call tracking record only — it does not yet update Belsmart's own CDR, lead status, or reporting. Wiring this into Belsmart's own reporting is coming soon.\n- Conference participants' `callerIdName` is always a generic placeholder from FreeSWITCH (\"Outbound Call\"), never a real per-party name — the roster relies on the `leadName` you pass into `addToConference()`, falling back to the raw number.\n- `listConferenceParticipants()` has no live/push updates of its own — membership and mute/hold state are only as fresh as the last call to it (the built-in widget covers this with its own polling; a custom UI needs to poll or re-call it itself).\n- Attended transfer has no built-in widget UI (a deliberate scope choice, not a bug) — only usable via a custom UI today.\n- A browser crash/force-quit/sudden network loss mid-call leaves the customer's leg connected with no agent on it — the `pagehide` best-effort hangup (see \"Connection resilience\") only covers a deliberate tab/browser close or navigation. Fully closing this gap needs a gateway-side keepalive/RTP timeout, which is outside the SDK's control.\n- Whether `POST /calls/:id/end` (used by the `pagehide` fallback above) actually terminates the live FreeSWITCH channel, or only records CDR metadata, is unconfirmed — see `PARTNER-INTEGRATION-QA.md`. If it's metadata-only, the `keepalive` beacon reports the call as ended without truly disconnecting the customer, and the SIP `BYE` remains the only signal that can actually hang it up.\n\n## Changelog\n\n### 0.11.0\n\n**Added**\n- Pause: `client.setStatus()`, `client.getStatus()`, the `\"status_changed\"` event, `SessionInfo.paused`, and a Pause / Resume button in the widget. Needs gateway endpoint `PUT /partner-api/v1/session/status`.\n- Campaign switching: `client.listCampaigns()`, `client.getCampaign()`, `client.setCampaign(campaignUuid)`, the `\"campaign_changed\"` event and `SessionInfo.campaign`. The built-in widget has the campaign picker under a settings (gear) icon in its header. Needs gateway endpoints `GET /partner-api/v1/campaigns` and `PUT /partner-api/v1/session/campaign`.\n- `BelsmartCrmClient.listDispositions(credentials, agentUuid?)` — optional `agentUuid` returns the dispositions of that agent's picked campaign.\n- Custom caller ID: `CallOptions.callerId`, `CampaignInfo.allowCustomCallerId`, `client.isCustomCallerIdAllowed()`, and a \"Your caller ID\" field in the widget for allowed campaigns. Needs the gateway's Allow Custom Caller ID permission (master admin).\n\n**Fixed**\n- Agent-to-agent transfers (and conferences) from another partner-SDK agent no longer fail: FreeSWITCH carries the transferring agent's `P-uniqueId` onto the leg it sends the receiving agent, and the receiving SDK rejected any unexpected `P-uniqueId` as a stale bridge leg of its own (486). Only this device's own recent call ids are treated as stale now; the transfer arrives as a normal `\"incoming\"` call.\n\n### 0.9.0\n\n**Added**\n- `session_expired` event — fires when a REST call 401s with a gateway-confirmed\n  `error: \"session_expired\"` body, distinguishing an expired session from any other request\n  failure (`SessionExpiredError`, exported from the client's internals).\n- Automatic session refresh: when constructed with `apiKey`/`apiSecret` (not a pre-fetched\n  `BelsmartCrmSessionConfig`), the SDK silently re-mints a new session at ~90% of the current\n  one's TTL, so a 12h shift-length session should now rarely, if ever, actually hit expiry.\n  `session_expired` still fires for the cases auto-refresh can't cover — a\n  `BelsmartCrmSessionConfig` has no secret in the browser to remint with, or auto-refresh itself\n  exhausts its retries.\n- A call-active heartbeat (`PATCH /calls/:id/heartbeat`, ~20s) sent for the duration of every\n  call, so the gateway can tell a crashed/force-closed browser apart from a call that's just\n  quiet, and force-end an abandoned customer leg (~60-80s) instead of leaving it connected\n  indefinitely — closes the gap noted in 0.8.1's \"Known limitations\" for the crash/force-quit/\n  network-loss case.\n- A session-presence heartbeat (`PATCH /session/heartbeat`, ~60s, independent of any call) plus\n  an explicit best-effort sign-off (`POST /session/offline`) on `destroy()`/`pagehide`, so the\n  gateway's live dashboard reflects this agent's online/idle/logged-out state with the same\n  fidelity as its browser extension.\n\n### 0.8.1\n\n**Added**\n- `BelsmartCrmSessionConfig` — construct `BelsmartCrmClient` with a pre-fetched `SessionInfo` instead of `apiKey`/`apiSecret`, so a backend-side session exchange (`POST /partner-api/v1/session`) can keep the tenant secret out of the browser entirely. See \"Keeping `apiSecret` off the browser\". `ApiClient.createSession()`/browser-side key pair usage is unchanged and still supported for simpler integrations.\n- Best-effort call termination on the browser's `pagehide` event (agent tab/browser close or navigation, mid-call) — sends both a SIP `BYE` over the WS and a `keepalive: true` `POST /calls/:id/end`, to mitigate the customer's leg being left connected with no agent on it. Doesn't cover a crash/force-quit/network loss; see \"Connection resilience\" and \"Known limitations\".\n\n**Fixed**\n- A denied/blocked/missing microphone (or a mic already in use by another application) no longer leaves a call silently stuck on `\"connecting\"`/`\"ringing\"` forever. It's now caught where `sip.js` actually requests the microphone (accepting the bridge-leg INVITE for `call()`, or `IncomingCall.answer()`), and reported as a real `\"ended\"` plus a clear `\"error\"` event whose message names the actual problem (permission denied, no device, device busy) instead of surfacing nothing at all.\n\n### 0.7.0\n\n**Added**\n- `client.getCallCdr(callId?)` — the gateway's own CDR for a completed call.\n- `client.getCallRecording(callId?)` — the recording URL for a completed call, once the gateway has finished processing it.\n\n**Fixed**\n- The `\"ended\"` event now fires only after the gateway has recorded the call's `ended_at`/`duration` (the SDK now awaits its own `endCall()` report before emitting), instead of firing immediately and reporting that in the background. Previously, calling `getCallCdr()`/`getCallRecording()` from inside an `\"ended\"` handler could race that in-flight report and see a call that looked answered but not yet ended. This delays `\"ended\"` by one network round-trip to the gateway (normally well under 100ms) — if you were relying on it firing instantaneously for UI feedback, account for that small added latency.\n\n### 0.6.0\n\n**Added**\n- A country-code selector on the built-in widget's dial pad, pre-filled from `client.getDefaultCountryCode()` (the campaign's own country code, tenant portal) but always editable for a one-off call to a different country. `call()`/`addToConference()`/`startConsult()` themselves are unaffected — they still dial `toNumber` exactly as given.\n- `client.getDefaultCountryCode()`.\n\n**Fixed**\n- Tightened the customer-answer detection poll (gateway and SDK sides) from 1.5s to 400ms — cut the delay between the customer actually answering and the widget's duration timer starting from up to ~3s down to well under a second.\n\n### 0.5.0\n\n**Added**\n- Conference calling: `addToConference()`, `hangupAll()`, `listConferenceParticipants()`, per-participant mute/hold/kick, and a roster UI in the built-in widget — with continuous background refresh (every 5s) so someone hanging up on their own drops off the roster automatically.\n- Attended transfer: `startConsult()`, `completeTransfer()`, `cancelConsult()` (SDK-only, no built-in widget UI yet).\n- A real `\"ringing\"` call state, distinct from the agent's own leg connecting — `\"active\"` now fires only once the customer actually answers (detected server-side; see \"Call lifecycle\" above). **This is a behavior change** from earlier versions, where `\"active\"` fired as soon as your own leg connected.\n- DTMF keypad button in the built-in widget (`sendDtmf()` itself already existed).\n- `client.getAgentLabel()`.\n- `debug` config option and the SDK's own structured internal logging, replacing sip.js's raw wire-level SIP logs (always suppressed now, regardless of `debug`).\n\n**Fixed**\n- The built-in widget's buttons could go permanently unresponsive after the widget was dragged even once (a capture-phase click guard never reset its flag after first use).\n- The widget rebuilt its entire DOM every second just to tick the duration display, which could occasionally eat a button click if a click's mousedown/mouseup straddled a rebuild.\n- sip.js's own built-in transport reconnect never actually ran at all — `reconnectionAttempts`/`reconnectionDelay` were set on the wrong options object (`transportOptions` instead of top-level `UserAgentOptions`), so the effective value silently stayed at sip.js's default of `0` (\"never attempt\"). Corrected, and backed by the new watchdog/`online`-event mechanism described in \"Connection resilience\".\n- A stale/abandoned call attempt's INVITE (e.g. one that arrived late after you'd already hung up or moved on) could be misclassified as a genuine new incoming call. Now matched exactly against the `P-uniqueId` header Belsmart stamps on every self-originated bridge-leg INVITE, instead of a timing-window guess.\n- Holding a conference participant also showed them as muted — FreeSWITCH sets `can_speak: false` as a side effect of hold, which was being read as an explicit mute.\n- Call duration (both the stored value and, now that `\"active\"` means true answer, the widget's live timer) was measured from when `call()` was invoked, not from when the customer actually answered — could overstate duration by the full ring time.\n- A party added via `addToConference()` or consulted via `startConsult()` could join already muted — FreeSWITCH's default conference profile joins non-moderator members muted, which nothing in the gateway was correcting for. The gateway now waits for them to join and explicitly unmutes them.\n\n## Building\n\n```\nnpm install\nnpm run build   # outputs dist/index.js (cjs), dist/index.mjs (esm), dist/index.global.js (iife)\n```\n","readmeFilename":"README.md"}