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