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

1

Generate an embed token

Go to Dashboard > Settings > Embed Phone Widget. Enter an optional label (e.g. “Main Website”) and click Generate Embed Token.

The token is only displayed once. Copy it immediately and store it securely. If you lose it, revoke it and generate a new one.
2

Add the script tag

Paste the following snippet before the closing </body> tag of your website:

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.

3

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

AttributeRequiredDescription
srcYesURL to the widget script. Always use https://voice.epochdm.com/widget/phone.js
data-tokenYesYour 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.

text
Example: mysite.com, app.mysite.com, crm.mycompany.io
  • The check matches the exact hostname or any subdomain (e.g. mysite.com also allows www.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
For development, leave the domain list empty or add 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)boolean

Programmatically 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()void

Disconnect the active call.

EpochPhone.isReady()boolean

Returns true if the widget is in a state that can accept a new call (ready, completed, or failed).

EpochPhone.getState()string

Returns the current widget state. One of: loading, ready, dialing, ringing, in-progress, completed, failed, error.

EpochPhone.reset()void

Reset the widget to the ready state. Hangs up any active call first.

EpochPhone.sendDtmf(digit)void

Send a DTMF tone during an active call. Accepts a single character: 0-9, *, or #.

EpochPhone.toggle()void

Toggle the dialer panel open/closed.

EpochPhone.on(event, callback)void

Subscribe to a widget event. See Events below.

EpochPhone.off(event, callback)void

Unsubscribe from a widget event.

Events

EventPayloadDescription
readynoneWidget 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

html
<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.

javascript
// 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:

html
<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.

javascript
// 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:

html
<!-- 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)

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>
      <p class="slds-m-top_small slds-text-color_weak">
        Status: {widgetState}
      </p>
    </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;

    // 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)

html
<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.

json
// 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"]
  }]
}
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

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

ErrorCause & Fix
Invalid embed tokenThe data-token value is incorrect or does not start with ept_. Double-check the token you pasted.
Invalid or revoked tokenThe token has been revoked in the dashboard. Generate a new one.
Domain not allowedThe current page's domain is not in the token's whitelist. Update the allowed domains in Dashboard > Settings.
Organization plan does not support widgetsUpgrade to the Pro plan or above.
No phone number provisionedYour organization does not have a phone number yet. Set one up in Dashboard > Settings.
Failed to load phone SDKThe Twilio Voice SDK could not be loaded. Check for ad blockers or network restrictions blocking the script.
Widget not appearingEnsure 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.

Open raw Markdown
markdown
---
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. |