Integration Guide
Embed Phone Widget
Add a floating phone widget to any website or CRM so your team can make outbound calls directly from the browser using your business phone number.
Prerequisites
- An Epoch Voice AI account on the Pro plan or above
- A provisioned business phone number (set up in Dashboard > Settings)
- Access to the HTML of the website or CRM where you want to embed the widget
Quick Start
Generate an embed token
Go to Dashboard > Settings > Embed Phone Widget. Enter an optional label (e.g. “Main Website”) and click Generate Embed Token.
Add the script tag
Paste the following snippet before the closing </body> tag of your website:
<script src="https://voice.epochdm.com/widget/phone.js" data-token="ept_YOUR_TOKEN_HERE"></script>Replace ept_YOUR_TOKEN_HERE with your actual embed token.
Test the widget
Reload your page. A green phone button should appear in the bottom-right corner. Click it to expand the dialer panel and make a test call.
Embed Code Details
The widget is loaded via a single <script> tag. It automatically:
- Exchanges your embed token for a short-lived Twilio access token (1-hour TTL)
- Loads the Twilio Voice SDK
- Renders a floating action button (FAB) in the bottom-right corner
- Handles token refresh automatically when the session nears expiration
Attributes
| Attribute | Required | Description |
|---|---|---|
src | Yes | URL to the widget script. Always use https://voice.epochdm.com/widget/phone.js |
data-token | Yes | Your embed token (starts with ept_) |
Domain Whitelisting
When generating an embed token you can optionally specify a comma-separated list of allowed domains. If set, the widget will only work on pages served from those domains.
Example: mysite.com, app.mysite.com, crm.mycompany.io- The check matches the exact hostname or any subdomain (e.g.
mysite.comalso allowswww.mysite.com) - If the list is empty, the widget works on any domain
- Domain checks are enforced server-side — the widget cannot be spoofed client-side
localhost to allow local testing.JavaScript API Reference
The widget exposes a global API at window.EpochPhone (also available as window.EpochVoice). Use this to programmatically control the widget from your application code.
Methods
EpochPhone.call(number)→ booleanProgrammatically dial a phone number. Opens the panel and starts the call. Returns true if the call was initiated, false if the widget isn't ready.
EpochPhone.hangup()→ voidDisconnect the active call.
EpochPhone.isReady()→ booleanReturns true if the widget is in a state that can accept a new call (ready, completed, or failed).
EpochPhone.getState()→ stringReturns the current widget state. One of: loading, ready, dialing, ringing, in-progress, completed, failed, error.
EpochPhone.reset()→ voidReset the widget to the ready state. Hangs up any active call first.
EpochPhone.sendDtmf(digit)→ voidSend a DTMF tone during an active call. Accepts a single character: 0-9, *, or #.
EpochPhone.toggle()→ voidToggle the dialer panel open/closed.
EpochPhone.on(event, callback)→ voidSubscribe to a widget event. See Events below.
EpochPhone.off(event, callback)→ voidUnsubscribe from a widget event.
Events
| Event | Payload | Description |
|---|---|---|
ready | none | Widget is initialized and ready to make calls |
error | { message: string } | An error occurred (device error or call failure) |
callStarted | { number: string } | An outbound call has been initiated |
callEnded | { number: string, duration: number } | A call has ended. Duration is in seconds. |
Example: Click-to-Call Button
<button onclick="EpochPhone.call('+15551234567')">
Call Customer
</button>
<script>
// Listen for call events
EpochPhone.on('callStarted', function(data) {
console.log('Calling', data.number);
});
EpochPhone.on('callEnded', function(data) {
console.log('Call ended after', data.duration, 'seconds');
});
</script>CRM Integration Examples
Generic CRM — Click-to-Call Pattern
The most universal integration approach: add click-to-call buttons next to phone numbers in any CRM or contact list. This pattern works with any web-based CRM.
// Wait for the widget to be ready
EpochPhone.on('ready', function() {
// Find all phone number elements and make them clickable
document.querySelectorAll('[data-phone]').forEach(function(el) {
el.style.cursor = 'pointer';
el.style.color = '#059669';
el.title = 'Click to call with Epoch';
el.addEventListener('click', function() {
var number = el.getAttribute('data-phone');
EpochPhone.call(number);
});
});
});
// Log completed calls for CRM activity tracking
EpochPhone.on('callEnded', function(data) {
// POST call data to your CRM's API to create an activity record
fetch('/api/crm/log-call', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: data.number,
duration: data.duration,
timestamp: new Date().toISOString()
})
});
});In your HTML, tag phone numbers with a data-phone attribute:
<span data-phone="+15551234567">(555) 123-4567</span>HubSpot
Embed the widget into HubSpot using a custom CRM card or by injecting the script via a HubSpot Sales Extension or custom coded action.
Option A: HubSpot CRM Extension (Calling SDK)
Register a custom calling extension with HubSpot that launches the Epoch widget when a user clicks “Call” on a contact record.
// HubSpot Calling Extension SDK integration
// Register in your HubSpot app settings, then serve this page as your calling widget URL
const HubspotCalling = {
init() {
// Load the HubSpot Calling SDK
const script = document.createElement('script');
script.src = 'https://js.hubspot.com/calling-extensions-sdk.js';
script.onload = () => this.setup();
document.head.appendChild(script);
},
setup() {
const extensions = window.HubSpotCallingExtensions.create({
debugMode: false,
eventHandlers: {
onReady() {
extensions.initialized({
isLoggedIn: true,
sizeInfo: { width: 400, height: 600 }
});
},
onDialNumber(event) {
const { phoneNumber, objectId, objectType } = event;
// Use Epoch widget to make the call
if (EpochPhone.isReady()) {
EpochPhone.call(phoneNumber);
}
},
onEndCall() {
EpochPhone.hangup();
}
}
});
// Forward Epoch events to HubSpot
EpochPhone.on('callStarted', (data) => {
extensions.outgoingCall({
callStartTime: Date.now(),
phoneNumber: data.number,
createEngagement: true
});
});
EpochPhone.on('callEnded', (data) => {
extensions.callEnded();
extensions.callCompleted({
engagementId: null, // HubSpot will auto-create
hideWidget: false
});
});
}
};
HubspotCalling.init();Option B: Simple Script Injection
If you use a CMS page or custom module in HubSpot, simply add the embed script tag. Then add click-to-call buttons in your templates:
<!-- Add to HubSpot custom module or page footer -->
<script src="https://voice.epochdm.com/widget/phone.js"
data-token="ept_YOUR_TOKEN_HERE"></script>
<script>
// Auto-attach to HubSpot contact phone fields
EpochPhone.on('ready', function() {
document.querySelectorAll('.phone-number, [data-phone]').forEach(function(el) {
el.style.cursor = 'pointer';
el.addEventListener('click', function() {
EpochPhone.call(el.textContent.trim());
});
});
});
</script>Salesforce
Integrate the widget into Salesforce as a Lightning Web Component (LWC) or Visualforce page embedded in the utility bar.
Lightning Web Component (LWC)
<!-- epochDialer.html -->
<template>
<lightning-card title="Epoch Dialer" icon-name="utility:call">
<div class="slds-p-around_medium">
<lightning-input
type="tel"
label="Phone Number"
value={phoneNumber}
onchange={handlePhoneChange}>
</lightning-input>
<lightning-button
variant="brand"
label="Call"
onclick={handleCall}
class="slds-m-top_small">
</lightning-button>
<p class="slds-m-top_small slds-text-color_weak">
Status: {widgetState}
</p>
</div>
</lightning-card>
</template>// epochDialer.js
import { LightningElement, api, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
export default class EpochDialer extends LightningElement {
@api recordId;
@track phoneNumber = '';
@track widgetState = 'loading';
renderedCallback() {
if (this._initialized) return;
this._initialized = true;
// Load Epoch widget — upload phone.js as a Static Resource
loadScript(this, '/resource/EpochWidget')
.then(() => {
// The widget auto-initializes; listen for ready
window.EpochPhone.on('ready', () => {
this.widgetState = 'ready';
});
window.EpochPhone.on('callStarted', (data) => {
this.widgetState = 'in-progress';
this.logCallActivity(data.number, 'started');
});
window.EpochPhone.on('callEnded', (data) => {
this.widgetState = 'completed';
this.logCallActivity(data.number, 'completed', data.duration);
});
});
}
handlePhoneChange(event) {
this.phoneNumber = event.target.value;
}
handleCall() {
if (window.EpochPhone && window.EpochPhone.isReady()) {
window.EpochPhone.call(this.phoneNumber);
}
}
logCallActivity(phone, status, duration) {
// Use Salesforce Apex to create a Task record
// createCallTask({ contactId: this.recordId, phone, status, duration });
}
}Visualforce Page (Classic)
<apex:page>
<script src="https://voice.epochdm.com/widget/phone.js"
data-token="ept_YOUR_TOKEN_HERE"></script>
<div style="padding: 20px;">
<h2>Epoch Phone</h2>
<input type="tel" id="sf-phone" placeholder="Enter number" />
<button onclick="handleSFCall()">Call</button>
</div>
<script>
function handleSFCall() {
var phone = document.getElementById('sf-phone').value;
if (phone && EpochPhone.isReady()) {
EpochPhone.call(phone);
}
}
// Log calls back to Salesforce
EpochPhone.on('callEnded', function(data) {
// Use Salesforce AJAX toolkit or Visualforce remoting
// to create a Task record
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.CallController.logCall}',
data.number,
data.duration,
function(result) { console.log('Call logged:', result); }
);
});
</script>
</apex:page>Chrome Extension Pattern
Build a Chrome extension that injects the Epoch widget into any CRM or web page. Useful when you cannot modify the CRM's HTML directly.
// manifest.json (Manifest V3)
{
"manifest_version": 3,
"name": "Epoch Click-to-Call",
"version": "1.0",
"permissions": [],
"content_scripts": [{
"matches": ["https://app.hubspot.com/*", "https://*.salesforce.com/*"],
"js": ["content.js"]
}]
}// content.js
(function() {
var script = document.createElement('script');
script.src = 'https://voice.epochdm.com/widget/phone.js';
script.setAttribute('data-token', 'ept_YOUR_TOKEN_HERE');
document.body.appendChild(script);
})();Security & Token Management
Token Storage
Embed tokens are hashed (SHA-256) before storage. We never store the raw token. The token is shown to you exactly once when generated — after that, only the prefix (ept_XXXX...) is visible in your dashboard.
Token Exchange
When the widget loads, it exchanges the embed token for a short-lived Twilio access token (1-hour TTL). The widget automatically refreshes the Twilio token before expiration, so users experience no interruptions.
Revoking a Token
Go to Dashboard > Settings > Embed Phone Widget and click the trash icon next to the token. Revocation takes effect immediately — any widget using that token will stop working.
Best Practices
- Use a separate token for each website or integration
- Always set domain whitelists in production
- Revoke unused tokens regularly
- Never expose tokens in public Git repositories
Troubleshooting
| Error | Cause & Fix |
|---|---|
Invalid embed token | The data-token value is incorrect or does not start with ept_. Double-check the token you pasted. |
Invalid or revoked token | The token has been revoked in the dashboard. Generate a new one. |
Domain not allowed | The current page's domain is not in the token's whitelist. Update the allowed domains in Dashboard > Settings. |
Organization plan does not support widgets | Upgrade to the Pro plan or above. |
No phone number provisioned | Your organization does not have a phone number yet. Set one up in Dashboard > Settings. |
Failed to load phone SDK | The Twilio Voice SDK could not be loaded. Check for ad blockers or network restrictions blocking the script. |
| Widget not appearing | Ensure the script tag is placed before </body>. Check the browser console for errors. Verify the src URL is correct. |
Machine-Readable Version
The full contents of this article are also available as structured Markdown for consumption by LLMs and automated tools.
---
title: Epoch Voice AI — Widget Integration Guide
version: 1.0
last_updated: 2025-03-13
url: https://voice.epochdm.com/docs/widget-integration
raw_markdown_url: https://voice.epochdm.com/docs/widget-integration/llm.md
---
# Epoch Voice AI — Widget Integration Guide
Embed a floating phone widget into any website or CRM. Your team can make outbound calls directly from the browser using your business phone number.
## Prerequisites
- Epoch Voice AI account on the **Pro plan or above**
- A provisioned business phone number (configured in Dashboard > Settings)
- Access to the HTML of the target website or CRM
## Quick Start
### Step 1: Generate an Embed Token
1. Log in to your Epoch Voice AI dashboard
2. Navigate to **Settings > Embed Phone Widget**
3. (Optional) Enter a label (e.g. "Main Website") and allowed domains
4. Click **Generate Embed Token**
5. Copy the token immediately — it is only shown once
### Step 2: Add the Script Tag
Add the following to your website, before the closing `</body>` tag:
```html
<script src="https://voice.epochdm.com/widget/phone.js" data-token="ept_YOUR_TOKEN_HERE"></script>
```
Replace `ept_YOUR_TOKEN_HERE` with your actual embed token.
### Step 3: Test
Reload the page. A green phone button appears in the bottom-right corner. Click it to open the dialer.
## Embed Code
| Attribute | Required | Description |
|-------------|----------|-------------|
| `src` | Yes | Always `https://voice.epochdm.com/widget/phone.js` |
| `data-token` | Yes | Your embed token (starts with `ept_`) |
The widget automatically:
- Exchanges the embed token for a short-lived Twilio access token (1-hour TTL)
- Loads the Twilio Voice SDK
- Renders a floating action button (FAB) in the bottom-right corner
- Refreshes the token before expiration
## Domain Whitelisting
When generating a token, you can specify allowed domains (comma-separated):
```
mysite.com, app.mysite.com, crm.mycompany.io
```
- Matches exact hostname or any subdomain (e.g. `mysite.com` also allows `www.mysite.com`)
- Empty list = works on any domain
- Enforced server-side
- Add `localhost` for development/testing
## JavaScript API Reference
The widget exposes a global API at `window.EpochPhone` (alias: `window.EpochVoice`).
### Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `EpochPhone.call(number)` | `boolean` | Dial a phone number. Opens panel and starts call. Returns true if initiated. |
| `EpochPhone.hangup()` | `void` | Disconnect the active call. |
| `EpochPhone.isReady()` | `boolean` | True if widget can accept a new call (state: ready, completed, or failed). |
| `EpochPhone.getState()` | `string` | Current state: `loading` \| `ready` \| `dialing` \| `ringing` \| `in-progress` \| `completed` \| `failed` \| `error` |
| `EpochPhone.reset()` | `void` | Reset to ready state. Hangs up active call first. |
| `EpochPhone.sendDtmf(digit)` | `void` | Send a DTMF tone during an active call (0-9, *, #). |
| `EpochPhone.toggle()` | `void` | Toggle dialer panel open/closed. |
| `EpochPhone.on(event, callback)` | `void` | Subscribe to a widget event. |
| `EpochPhone.off(event, callback)` | `void` | Unsubscribe from a widget event. |
### Events
| Event | Payload | Description |
|-------|---------|-------------|
| `ready` | none | Widget initialized and ready to make calls |
| `error` | `{ message: string }` | An error occurred |
| `callStarted` | `{ number: string }` | Outbound call initiated |
| `callEnded` | `{ number: string, duration: number }` | Call ended. Duration in seconds. |
### Example: Click-to-Call Button
```html
<button onclick="EpochPhone.call('+15551234567')">Call Customer</button>
<script>
EpochPhone.on('callStarted', function(data) {
console.log('Calling', data.number);
});
EpochPhone.on('callEnded', function(data) {
console.log('Call ended after', data.duration, 'seconds');
});
</script>
```
## CRM Integration Examples
### Generic CRM — Click-to-Call Pattern
```javascript
// Auto-attach click-to-call to elements with data-phone attribute
EpochPhone.on('ready', function() {
document.querySelectorAll('[data-phone]').forEach(function(el) {
el.style.cursor = 'pointer';
el.addEventListener('click', function() {
EpochPhone.call(el.getAttribute('data-phone'));
});
});
});
// Log calls to your CRM
EpochPhone.on('callEnded', function(data) {
fetch('/api/crm/log-call', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: data.number,
duration: data.duration,
timestamp: new Date().toISOString()
})
});
});
```
HTML:
```html
<span data-phone="+15551234567">(555) 123-4567</span>
```
### HubSpot — Calling Extension SDK
```javascript
// Register as a HubSpot Calling Extension
const extensions = window.HubSpotCallingExtensions.create({
eventHandlers: {
onReady() {
extensions.initialized({ isLoggedIn: true, sizeInfo: { width: 400, height: 600 } });
},
onDialNumber(event) {
const { phoneNumber } = event;
if (EpochPhone.isReady()) EpochPhone.call(phoneNumber);
},
onEndCall() {
EpochPhone.hangup();
}
}
});
EpochPhone.on('callStarted', (data) => {
extensions.outgoingCall({
callStartTime: Date.now(),
phoneNumber: data.number,
createEngagement: true
});
});
EpochPhone.on('callEnded', () => {
extensions.callEnded();
extensions.callCompleted({ hideWidget: false });
});
```
### HubSpot — Simple Script Injection
```html
<script src="https://voice.epochdm.com/widget/phone.js" data-token="ept_YOUR_TOKEN_HERE"></script>
<script>
EpochPhone.on('ready', function() {
document.querySelectorAll('.phone-number, [data-phone]').forEach(function(el) {
el.style.cursor = 'pointer';
el.addEventListener('click', function() {
EpochPhone.call(el.textContent.trim());
});
});
});
</script>
```
### Salesforce — Lightning Web Component (LWC)
```html
<!-- epochDialer.html -->
<template>
<lightning-card title="Epoch Dialer" icon-name="utility:call">
<div class="slds-p-around_medium">
<lightning-input type="tel" label="Phone Number" value={phoneNumber} onchange={handlePhoneChange}></lightning-input>
<lightning-button variant="brand" label="Call" onclick={handleCall} class="slds-m-top_small"></lightning-button>
</div>
</lightning-card>
</template>
```
```javascript
// epochDialer.js
import { LightningElement, api, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
export default class EpochDialer extends LightningElement {
@api recordId;
@track phoneNumber = '';
@track widgetState = 'loading';
renderedCallback() {
if (this._initialized) return;
this._initialized = true;
loadScript(this, '/resource/EpochWidget').then(() => {
window.EpochPhone.on('ready', () => { this.widgetState = 'ready'; });
window.EpochPhone.on('callEnded', (data) => {
this.widgetState = 'completed';
// Log activity via Apex
});
});
}
handlePhoneChange(event) { this.phoneNumber = event.target.value; }
handleCall() {
if (window.EpochPhone && window.EpochPhone.isReady()) {
window.EpochPhone.call(this.phoneNumber);
}
}
}
```
### Salesforce — Visualforce Page
```html
<apex:page>
<script src="https://voice.epochdm.com/widget/phone.js" data-token="ept_YOUR_TOKEN_HERE"></script>
<script>
EpochPhone.on('callEnded', function(data) {
Visualforce.remoting.Manager.invokeAction(
'{!$RemoteAction.CallController.logCall}',
data.number, data.duration,
function(result) { console.log('Logged:', result); }
);
});
</script>
</apex:page>
```
### Chrome Extension — Inject into Any CRM
```json
{
"manifest_version": 3,
"name": "Epoch Click-to-Call",
"version": "1.0",
"content_scripts": [{
"matches": ["https://app.hubspot.com/*", "https://*.salesforce.com/*"],
"js": ["content.js"]
}]
}
```
```javascript
// content.js
(function() {
var script = document.createElement('script');
script.src = 'https://voice.epochdm.com/widget/phone.js';
script.setAttribute('data-token', 'ept_YOUR_TOKEN_HERE');
document.body.appendChild(script);
})();
```
## Security & Token Management
- **Storage**: Tokens are SHA-256 hashed before storage. Raw tokens are never stored.
- **Token exchange**: Widget exchanges embed token for a 1-hour Twilio access token. Auto-refreshes before expiration.
- **Revocation**: Revoke tokens in Dashboard > Settings > Embed Phone Widget. Takes effect immediately.
- **Best practices**:
- Use a separate token per website/integration
- Always set domain whitelists in production
- Revoke unused tokens regularly
- Never commit tokens to Git repositories
## Troubleshooting
| Error | Cause & Fix |
|-------|------------|
| `Invalid embed token` | Token is incorrect or doesn't start with `ept_`. Double-check the value. |
| `Invalid or revoked token` | Token was revoked. Generate a new one in Dashboard > Settings. |
| `Domain not allowed` | Page domain not in token's whitelist. Update allowed domains in Settings. |
| `Organization plan does not support widgets` | Upgrade to Pro plan or above. |
| `No phone number provisioned` | Set up a phone number in Dashboard > Settings. |
| `Failed to load phone SDK` | Twilio SDK blocked. Check ad blockers or network restrictions. |
| Widget not appearing | Ensure script is before `</body>`. Check browser console for errors. |