@avibra/pulse-web-sdk
v1.1.4
Published
Pulse — AI voice feedback SDK for third-party web applications
Maintainers
Readme
Pulse SDK — AI-Powered Voice Feedback
Embed AI-powered voice feedback conversations into any web application in minutes.
Table of Contents
- Overview
- Quick Start
- API Reference
- Theme Customization
- Analytics Platform Integration
- Security Model
- Performance
- Backend API Contract
- Events Reference
- Troubleshooting
Overview
Pulse is an AI-powered voice feedback platform. When a user performs a significant action — cancels a subscription, completes onboarding, or churns — Pulse surfaces a conversational voice prompt so they can tell you why in their own words.
Two integration models:
| Model | How it works |
|---|---|
| SDK Tracking | Call Pulse.trackEvent() — SDK sends event to Pulse backend which evaluates triggers |
| Analytics Webhook | Mixpanel / Segment / Braze forwards events to Pulse backend; SDK polls for pending triggers |
Quick Start
CDN (Recommended for most partners)
<!-- Add before </body> -->
<script src="https://cdn.avibra.com/sdk/v1/pulse.min.js"></script>
<script>
Pulse.init({
partnerId: 'YOUR_PARTNER_ID',
userId: currentUser.id,
theme: {
primary: '#4F46E5',
onPrimary: '#FFFFFF',
background: '#FFFFFF',
surface: '#F9FAFB',
accent: '#22C55E',
danger: '#EF4444',
},
});
</script>npm / ESM
npm install @avibra/pulse-web-sdkimport { Pulse } from '@avibra/pulse-web-sdk';
Pulse.init({
partnerId: 'YOUR_PARTNER_ID',
userId: currentUser.id,
});API Reference
Pulse.init(config)
Initializes the SDK. Must be called once before any other method.
Pulse.init({
partnerId: 'app_abc123', // Required — from Pulse dashboard
userId: 'user_xyz', // Required — your user's unique ID
apiKey: 'pk_live_...', // Optional — sent as X-Api-Key on all requests
mode: 'prod', // Optional — 'staging' | 'st2' | 'prod' (default: 'prod')
baseUrl: 'https://...', // Optional — override API base URL (takes precedence over mode)
theme: { // Optional — see Theme Customization
primary: '#4F46E5',
onPrimary: '#FFFFFF',
background: '#FFFFFF',
surface: '#F9FAFB',
accent: '#22C55E',
danger: '#EF4444',
borderRadius: 16,
fontFamily: "'Inter', sans-serif",
},
debug: false, // Optional — enable console logging
pollIntervalMs: 30000, // Optional — trigger poll interval (default 30s)
maxOfflineQueueSize: 100, // Optional — offline buffer size
retry: {
maxAttempts: 3, // Optional — retry attempts on failure
baseDelayMs: 500, // Optional — initial backoff delay
maxDelayMs: 10000, // Optional — max backoff cap
},
});Environments
| mode | Base URL |
|---|---|
| prod (default) | https://pulse.avibra.com/api/v1 |
| staging | https://pulse.avibra.com/staging/api/v1 |
| st2 | https://pulse.avibra.com/st2/api/v1 |
If
baseUrlis also provided, it takes precedence overmode.
Pulse.trackEvent(name, properties?)
Track a user action. The SDK sends this to the Pulse backend which evaluates your trigger rules. If a survey is triggered, the widget appears automatically.
// On account close button click:
Pulse.trackEvent('ACCOUNT_CLOSE_CLICKED', {
plan: 'enterprise',
tenure_days: 365,
});
// On feature toggle:
Pulse.trackEvent('FEATURE_DISABLED', { feature: 'two-factor-auth' });Built-in protections:
- Max 100 events/minute (sliding window rate limit)
- 500ms debounce per event name (prevents double-fires)
- Fire-and-forget (never blocks your UI thread)
Pulse.identifyUser(payload)
Associate the current user with profile attributes. Call after login or when user context changes.
Pulse.identifyUser({
first_name: 'Jane',
last_name: 'Doe',
email: '[email protected]',
attributes: {
company: 'Acme Corp',
employee_count: 500,
region: 'EMEA',
},
});Pulse.startVoiceSurvey(signalId)
Programmatically launch a voice survey within the widget. Useful for testing or custom trigger logic.
button.addEventListener('click', async () => {
await Pulse.startVoiceSurvey('signal_churn_intent');
});Pulse.startCall(config)
Launch a standalone voice call session — no widget, no trigger polling. Designed for dedicated call pages (e.g. connect-pulse.avibra.com).
Pulse.startCall({
partnerId: 'app_abc123', // Required
userId: 'user_xyz', // Required
signalId: 'signal_123', // Required
mode: 'prod', // Optional — 'staging' | 'st2' | 'prod'
apiKey: 'pk_live_...', // Optional
theme: { ... }, // Optional
debug: false, // Optional
});Flow: Shows a loading screen while fetching signal details, then transitions directly to the ringing screen. If the signal fetch fails, an error screen is shown.
Typical usage — reading params from the URL hash:
<script src="https://cdn.avibra.com/sdk/v1/pulse.min.js"></script>
<script>
const params = new URLSearchParams(location.hash.slice(1));
Pulse.startCall({
partnerId: params.get('pid'),
userId: params.get('uid'),
signalId: params.get('sid'),
mode: params.get('m') || 'prod',
});
</script>URL format: https://connect-pulse.avibra.com/#pid=abc&uid=xyz&sid=signal_123&m=prod
Pulse.destroy()
Tear down the SDK completely. Removes the widget, stops polling, and clears all intervals.
// On logout, user switch, or SPA route teardown:
Pulse.destroy();Theme Customization
Pulse uses CSS design tokens inside a Shadow DOM to guarantee zero conflicts with your application styles.
Available Tokens
| Token | CSS Variable | Description |
|---|---|---|
| primary | --pulse-primary | Brand color for buttons, waveform |
| onPrimary | --pulse-on-primary | Text/icon color on primary backgrounds |
| background | --pulse-background | Widget outer background |
| surface | --pulse-surface | Inner card/surface color |
| accent | --pulse-accent | Success states, active indicators |
| danger | --pulse-danger | End-call, destructive actions |
| borderRadius | --pulse-border-radius | Widget corner rounding (px) |
| fontFamily | --pulse-font-family | Font family override |
Example: Dark Theme
Pulse.init({
partnerId: 'app_123',
userId: 'user_123',
theme: {
primary: '#6366F1',
onPrimary: '#FFFFFF',
background: '#1E1E2E',
surface: '#2D2D44',
accent: '#10B981',
danger: '#F43F5E',
borderRadius: 20,
fontFamily: "'Inter', sans-serif",
},
});Example: Corporate / Neutral Theme
theme: {
primary: '#0F172A',
onPrimary: '#FFFFFF',
background: '#FAFAFA',
surface: '#F1F5F9',
accent: '#0EA5E9',
danger: '#DC2626',
}Analytics Platform Integration
If you already use Mixpanel, Braze, or Segment, you don't need to call Pulse.trackEvent() at all.
Setup Steps
Configure a webhook in your analytics platform to forward events to:
POST https://pulse.avibra.com/api/v1/webhooks/segment # Segment POST https://pulse.avibra.com/api/v1/webhooks/mixpanel # Mixpanel POST https://pulse.avibra.com/api/v1/webhooks/braze # BrazeInclude the Pulse SDK in your app (init only, no tracking):
Pulse.init({ partnerId: 'YOUR_PARTNER_ID', userId: currentUser.id }); // SDK will poll GET /triggers every 30s and show widget when a trigger firesSet trigger rules in the Pulse dashboard linking your event names to surveys.
Security Model
API Key Authentication
When apiKey is provided, it is sent on every outbound request as the X-Api-Key header.
Headers attached to every request:
X-Pulse-Partner-Id— your application IDX-Pulse-Nonce— random 16-byte hex nonce (replay protection)X-Pulse-Timestamp— ISO 8601 timestampX-Api-Key— your API key (whenapiKeyis set)
⚠️ Important: Treat your
apiKeyas a secret. Avoid hardcoding it in publicly accessible source code. For production web apps, serve it from an authenticated backend endpoint.
Shadow DOM Isolation
The Pulse widget uses attachShadow({ mode: 'closed' }). External JavaScript cannot access the widget's internals, preventing CSS injection or DOM tampering.
Performance
| Metric | Target | How achieved |
|---|---|---|
| Bundle size (gzipped) | < 50KB | VoiceSessionManager lazy-loaded via import() |
| Init time | < 100ms | Synchronous module setup, async API calls |
| Main thread blocking | None | All API calls are fire-and-forget |
| Offline resilience | 100 events buffered | OfflineQueue with localStorage + reconnect drain |
Backend API Contract
| Method | Endpoint | Description |
|---|---|---|
| POST | /users/track | Track a user event |
| POST | /identify | Update user profile |
| GET | /triggers?userId= | Check for pending survey trigger |
| POST | /voice/session/start | Start a voice survey session |
| POST | /voice/session/audio | Send audio chunk (multipart/form-data) |
All endpoints return:
{ "success": true, "data": { ... } }
// or
{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "..." } }Events Reference
Listen to Pulse widget events on the document:
document.addEventListener('pulse:prompt-shown', (e) => {
console.log('Prompt shown for survey:', e.detail.signalId);
});
document.addEventListener('pulse:survey-accepted', (e) => {
// User clicked "Yes, Let's Go"
analytics.track('Pulse Survey Accepted', { signalId: e.detail.signalId });
});
document.addEventListener('pulse:survey-declined', (e) => {
// User clicked "Maybe Later"
});
document.addEventListener('pulse:session-completed', (e) => {
console.log(`Session completed in ${e.detail.duration}s`);
});Troubleshooting
Widget not appearing
- Ensure
Pulse.init()completed without errors (debug: truewill log all activity). - Verify your trigger rules are configured in the Pulse dashboard.
- Check that your backend is returning
hasPendingTrigger: truefromGET /triggers.
Microphone not working
- The browser requires HTTPS for
navigator.mediaDevices.getUserMedia. - Check that the user has granted microphone permissions in browser settings.
- Safari requires a user gesture (button click) before requesting mic access.
Events not reaching backend
- Check the offline queue: if
navigator.onLineis false, events are buffered. - Enable
debug: trueand inspect[Pulse][INFO]console logs. - Verify
partnerIdis correct and your backend is reachable atbaseUrl.
TypeScript types
All public types are exported from the package:
import type { PulseConfig, ThemeConfig, IdentifyPayload, SurveyTrigger } from '@avibra/pulse-web-sdk';Local Development
cd sdk
npm install
npm run build:watch # Rebuild on change
npm test # Run test suite
npm run lint # Lint TypeScriptOpen examples/basic-integration.html in a browser (after building) to test the widget locally.
License
MIT © Avibra Engineering
