Documentation

Hearrd Developer Guide

Everything you need to embed Hearrd voice feedback into your product and understand what your users are telling you.

Overview

Hearrd adds a feedback button to your product. When a user taps it, they can speak for up to 60 seconds or type their thoughts. Hearrd automatically transcribes voice recordings, summarises the feedback, and categorises it - as a bug report, feature request, churn risk, or praise — then sends it to your dashboard immediately.

The entire integration is a single <script> tag. There is no npm package to install, no build step, and no framework dependency. The widget is a standard browser script that works independently of whatever technology your product uses.

The widget URL is always https://api.hearrd.co/widget/widget.js. You embed this in your product. Your users interact with it. You see the results in your Hearrd dashboard.

Hearrd works on any web-based product - desktop browsers and mobile browsers. Native iOS and Android app support is on the roadmap. You'll be notified when the SDK ships.

Quickstart

Get Hearrd working in your product in under two minutes.

Step 1 - Create a project

Sign up at hearrd.co and create a project. You'll get an API key for that project — it looks like vxn_abc123...

Step 2 - Add the script tag to your product

html
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key_here"
></script>

Paste this before the closing </body>tag in your app. That's all. A button appears in the corner of your product.

Step 3 - Check your dashboard

Go to your project in the Hearrd dashboard. Feedback submitted through the widget appears there automatically, already categorised and summarised.

HTML / Any website

If your product runs on plain HTML, a CMS, Webflow, Framer, WordPress, or any tool that lets you add custom HTML — this is all you need:

html
<!-- Add before </body> -->
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
  data-prompt="What's one thing you'd change?"
></script>

The widget loads itself, injects its own styles, and is ready to use. Nothing else is required.

Plain JavaScript

If your product is built with plain JavaScript — no framework — you can add the script tag directly in your HTML, or load it programmatically from your JS file:

javascript
// Option 1: Direct HTML (simplest)
// Add to your HTML file before </body>
// <script src="https://api.hearrd.co/widget/widget.js" data-voxen-key="vxn_your_key"></script>

// Option 2: Load from JavaScript
function loadVoxen(apiKey, userId) {
  var script = document.createElement('script');
  script.src = 'https://api.hearrd.co/widget/widget.js';
  script.setAttribute('data-voxen-key', apiKey);
  if (userId) script.setAttribute('data-user-id', userId);
  document.body.appendChild(script);
}

// Call it when your app initialises
loadVoxen('vxn_your_key', currentUser ? currentUser.id : null);

TypeScript

For TypeScript projects, the approach is the same as plain JavaScript. If you want type safety for the global window.hearrd object, add this declaration to a .d.ts file in your project:

typescript
// hearrd.d.ts (create this file in your src/ folder)
interface HearrdInstance {
  show: () => void;
  hide: () => void;
  identify: (userId: string, userEmail?: string) => void;
  destroy: () => void;
}

declare global {
  interface Window {
    hearrd?: HearrdInstance;
  }
}

Then load the widget anywhere in your TypeScript code:

typescript
function loadVoxen(config: {
  apiKey: string;
  userId?: string;
  userEmail?: string;
  prompt?: string;
  position?: 'bottom-right' | 'bottom-left';
}): void {
  const script = document.createElement('script');
  script.src = 'https://api.hearrd.co/widget/widget.js';
  script.setAttribute('data-voxen-key', config.apiKey);
  if (config.userId) script.setAttribute('data-user-id', config.userId);
  if (config.userEmail) script.setAttribute('data-user-email', config.userEmail);
  if (config.prompt) script.setAttribute('data-prompt', config.prompt);
  if (config.position) script.setAttribute('data-position', config.position);
  document.body.appendChild(script);
}

// Use it
loadVoxen({
  apiKey: 'vxn_your_key',
  userId: currentUser.id,
  userEmail: currentUser.email,
});

React & Next.js

In React and Next.js, load the script inside useEffectso it only runs in the browser after the component mounts. Never load it at the top level of a component — it won't work during server-side rendering.

Next.js App Router

Add this to your root layout so the widget appears on every page:

tsx
'use client';
// app/layout.tsx
import { useEffect } from 'react';

export default function RootLayout({ children }) {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(script);
    return () => script.remove();
  }, []);

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Next.js App Router - with logged-in user

tsx
'use client';
import { useEffect } from 'react';

export default function RootLayout({ children }) {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');

    if (currentUser) {
      script.setAttribute('data-user-id', currentUser.id);
      script.setAttribute('data-user-email', currentUser.email);
    }

    document.body.appendChild(script);
    return () => script.remove();
  }, [currentUser?.id]);

  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Next.js Pages Router

tsx
// pages/_app.tsx
import { useEffect } from 'react';

export default function App({ Component, pageProps }) {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(script);
  }, []);

  return <Component {...pageProps} />;
}

Create React App

tsx
// src/App.tsx
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(script);
    return () => script.remove();
  }, []);

  return <div>Your app</div>;
}

Vue & Nuxt

Vue 3 (Composition API)

vue
<!-- App.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue';

let script;

onMounted(() => {
  script = document.createElement('script');
  script.src = 'https://api.hearrd.co/widget/widget.js';
  script.setAttribute('data-voxen-key', 'vxn_your_key');
  document.body.appendChild(script);
});

onUnmounted(() => script?.remove());
</script>

Vue 2 (Options API)

javascript
// In your main App component
export default {
  mounted() {
    const script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(script);
    this._voxenScript = script;
  },
  beforeDestroy() {
    this._voxenScript?.remove();
  },
}

Nuxt 3

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: 'https://api.hearrd.co/widget/widget.js',
          'data-voxen-key': 'vxn_your_key',
          defer: true,
        },
      ],
    },
  },
})

Svelte & SvelteKit

Svelte

svelte
<!-- App.svelte -->
<script>
  import { onMount, onDestroy } from 'svelte';

  let script;

  onMount(() => {
    script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(script);
  });

  onDestroy(() => script?.remove());
</script>

SvelteKit

svelte
<!-- src/routes/+layout.svelte -->
<script>
  import { onMount } from 'svelte';
  import { browser } from '$app/environment';

  onMount(() => {
    if (!browser) return;
    const script = document.createElement('script');
    script.src = 'https://api.hearrd.co/widget/widget.js';
    script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(script);
    return () => script.remove();
  });
</script>

<slot />

Angular

typescript
// app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>',
})
export class AppComponent implements OnInit, OnDestroy {
  private script: HTMLScriptElement | null = null;

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

  ngOnInit(): void {
    if (!isPlatformBrowser(this.platformId)) return;

    this.script = document.createElement('script');
    this.script.src = 'https://api.hearrd.co/widget/widget.js';
    this.script.setAttribute('data-voxen-key', 'vxn_your_key');
    document.body.appendChild(this.script);
  }

  ngOnDestroy(): void {
    this.script?.remove();
  }
}

Django, Flask & FastAPI

In server-rendered Python apps, the widget is a script tag in your HTML template. No Python package is required.

Django

html
{# base.html — add before </body> #}
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
  data-user-id="{{ request.user.pk }}"
  data-user-email="{{ request.user.email }}"
></script>
</body>

Show it only to authenticated users:

html
{% if user.is_authenticated %}
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
  data-user-id="{{ user.pk }}"
  data-user-email="{{ user.email }}"
></script>
{% endif %}

FastAPI / Jinja2

FastAPI uses Jinja2 templates the same way Flask does. Install Jinja2 and pass the request to your template, then add the script tag to your base template. No Python package or pip install is required for Hearrd.

python
# main.py
from fastapi import FastAPI, Request, Depends
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
 
app = FastAPI()
templates = Jinja2Templates(directory="templates")
 
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
    # Pass current_user however your auth works
    # e.g. using fastapi-users, JWT, or session middleware
    return templates.TemplateResponse("base.html", {
        "request": request,
        "current_user": request.state.user  # adjust to your setup
    })
html
{# templates/base.html — add before </body> #}
{% if current_user %}
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
  data-user-id="{{ current_user.id }}"
  data-user-email="{{ current_user.email }}"
></script>
{% else %}
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
></script>
{% endif %}
</body>
FastAPI with Jinja2 renders HTML server-side — the widget tag goes in your template exactly as it would in Django or Flask. If you use FastAPI as an API-only backend with a separate React/Vue frontend, follow the React or Vue guide instead and pass the user identity via the JavaScript attributes.

Configuration

All configuration is done through attributes on the script tag. There are no config files, no JavaScript objects, and no setup required beyond what's shown below.

AttributeRequiredDefaultDescription
data-voxen-keyYesYour project API key from the Hearrd dashboard
data-promptNoWhat's one thing you'd change?The question shown to users inside the widget
data-positionNobottom-rightWidget button position: bottom-right or bottom-left
data-user-idNoYour internal user ID — links this feedback to a user in your dashboard
data-user-emailNoUser's email address — useful for follow-up
data-hide-buttonNofalseSet to "true" to hide the floating button and trigger the widget manually

Identifying users

By default, all feedback is anonymous. To know which user left which piece of feedback, pass their ID and email through the script tag attributes.

When you do this, the feedback entry in your dashboard shows the user's details, so you can follow up directly or connect feedback to your existing user records.

html
<!-- Static values -->
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
  data-user-id="user_12345"
  data-user-email="user@example.com"
></script>

In server-rendered apps, inject the values from your backend:

html
<!-- Django -->
data-user-id="{{ request.user.pk }}"
data-user-email="{{ request.user.email }}"

<!-- Flask -->
data-user-id="{{ current_user.id }}"
data-user-email="{{ current_user.email }}"

<!-- PHP -->
data-user-id="<?= htmlspecialchars($user->id) ?>"
data-user-email="<?= htmlspecialchars($user->email) ?>"

<!-- Rails (ERB) -->
data-user-id="<%= current_user.id %>"
data-user-email="<%= current_user.email %>"

<!-- Blade (Laravel) -->
data-user-id="{{ auth()->user()->id }}"
data-user-email="{{ auth()->user()->email }}"

<!-- Handlebars / Mustache -->
data-user-id="{{ userId }}"
data-user-email="{{ userEmail }}"

<!-- EJS (Node.js) -->
data-user-id="<%= user.id %>"
data-user-email="<%= user.email %>"
User identity is only visible to you in your dashboard. It is never shown to other users or made public.

Programmatic control

Once the widget script has loaded, a window.hearrd object becomes available. You can use it to control the widget from your own JavaScript at any time.

javascript
// Open the widget (show the feedback dialog)
window.hearrd?.show();

// Close the widget
window.hearrd?.hide();

// Update the user's identity after they log in
window.hearrd?.identify('user_123', 'user@example.com');

// Remove the widget from the page entirely
window.hearrd?.destroy();

Opening the widget from your own button

You can suppress the default button and trigger the widget from any element in your product. This is useful if you want to place a feedback button inside your own UI.

html
<!-- Hide the default button -->
<script
  src="https://api.hearrd.co/widget/widget.js"
  data-voxen-key="vxn_your_key"
  data-hide-button="true"
></script>

<!-- Your own button, anywhere in your UI -->
<button onclick="window.hearrd?.show()">
  Give feedback
</button>

Waiting for the widget to load

If you need to call window.hearrd immediately after the page loads, wait for the script to finish loading:

javascript
const script = document.createElement('script');
script.src = 'https://api.hearrd.co/widget/widget.js';
script.setAttribute('data-voxen-key', 'vxn_your_key');
script.setAttribute('data-hide-button', 'true');

// This runs once the widget is ready
script.onload = function() {
  // Widget is now available
  window.hearrd?.identify(currentUser.id, currentUser.email);
};

document.body.appendChild(script);

The dashboard

Every piece of feedback submitted through your widget appears in your Hearrd dashboard. Here is what you see for each response:

CategoryBug, Feature request, Churn risk, or Praise — determined automatically
SummaryOne-sentence summary of what the user said
TranscriptFull word-for-word transcription of the recording
Audio playbackListen to the original recording (Pro and Team plans)
SentimentWhether the overall tone was positive, neutral, or negative
Churn risk flagA warning when a user sounds like they might stop using your product
User identityEmail and user ID, if you passed them via the script tag
Timestamp and durationWhen it was submitted and how long the recording was

The dashboard also shows weekly totals, a tag breakdown, and a churn risk count across all responses for each project.

Privacy & consent

Voice recordings are personal data. Here is what Hearrd handles automatically and what you need to do.

What Hearrd handles

Shows a consent notice inside the widget before any recording begins
Requires the user to explicitly grant microphone permission - this cannot be bypassed
Transmits all audio over HTTPS
Stores audio encrypted at rest
Allows deletion of any individual response from your dashboard at any time

What you need to do

Mention in your Privacy Policy that you collect voice feedback via Hearrd, a third-party tool
State how long recordings are kept (7 days on Free, 90 days on Pro and Team)
If a user requests deletion of their data, delete their response from your Hearrd dashboard

Plans & limits

Hearrd counts feedback responses per project per calendar month. When a project reaches its limit, the widget shows users a polite message that feedback is temporarily unavailable. Your product continues to work normally.

PlanPriceResponses / monthProjects
Free$0201
Pro$10 / month5003
Team$25 / monthUnlimitedUnlimited

You can upgrade at any time from Settings in your dashboard. The new limit applies immediately.