Skip to content

Embedding

Add the Speak AI recorder to any page. Includes the fix for Wix and Webflow, which strip microphone and camera permissions.

Updated View as MarkdownAsk ClaudeOpen in ChatGPTllms.txt

If the Embed recorder is not working on your site, such as Wix or Webflow, please follow the following steps.

Usually, websites like Wix or Webflow remove the Microphone and Camera permission which creates an issue for a recorder on any browser.

Please include the below script to include the permission in your iFrame dynamically.

<script>
    function addPermissions(iframe) {
        const ALLOW_PERMISSIONS = 'camera; microphone; fullscreen; display-capture';
        if (iframe.allow === ALLOW_PERMISSIONS) { return }
        iframe.setAttribute('allow', ALLOW_PERMISSIONS)
        iframe.allow = ALLOW_PERMISSIONS
        iframe.src = iframe.src
    }

    document.querySelectorAll('iframe').forEach(addPermissions);

    const observer = new MutationObserver((mutationsList, observer) => {
        for (let mutation of mutationsList) {
            if (mutation.addedNodes && mutation.addedNodes.length) {
                for (let node of mutation.addedNodes) {
                    if (node.querySelectorAll) {
               node.querySelectorAll('iframe').forEach(addPermissions)
                    }
                }
            }
        }
    })
    observer.observe(document.body, { attributes: false, childList: true, subtree: true })
</script>

Please ensure to include the above script to End of the Body to include the permission to all the iFrames on the page.

For Wix:

Here’re a few steps to follow for the Wix website.

Go to Settings and scroll to Advanced(the last section), and you can see Custom Code.

Include the script code under the body - END option.

That will ask you to apply on all the pages or specific pages.

Iframe Controls - Quick Start Guide

5-Minute Quick Start, Speak AI Recorder Embed


Test Embed Iframe

Step 1: Basic Iframe Embedding

<!-- Minimal Embedding -->
<iframe
  src="https://recorder.speakai.co/iframe/YOUR_TOKEN_HERE"
  allow="microphone; camera"
  width="100%"
  height="700px">
</iframe>

Step 2: Add Query Parameters

<!-- With Query Parameters -->
<iframe
  src="https://recorder.speakai.co/iframe/YOUR_TOKEN_HERE?hideWaveform=true&hideTitle=true&submitLabel=Send"
  allow="microphone; camera"
  width="100%"
  height="700px">
</iframe>

Step 3: Add PostMessage Control

// Get iframe reference
const iframe = document.querySelector('iframe');

// Start recording
iframe.contentWindow.postMessage({ action: 'start' }, 'https://recorder.speakai.co');

// Stop recording
iframe.contentWindow.postMessage({ action: 'stop' }, 'https://recorder.speakai.co');

// Listen for responses
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://recorder.speakai.co') return;
  const response = JSON.parse(event.data);
  console.log('Response:', response);
});

⚙️ Query Parameters Reference

Parameter Type Default Description
hideWaveform boolean false Hide audio waveform visualization
hideTitle boolean false Hide title/header text
submitLabel string “Upload” Custom submit button text
hideSubmit boolean false Hide submit button
preselect string “audio” | “video” | “upload” | “screenshare” Pre-select recording type
name string “” Pre-fill name field
email string “” Pre-fill email field
folderId string “” Pre-fill folder ID
field1 - field10 string “” Pre-fill custom question answers (up to 10)

Examples

Hide waveform:
?hideWaveform=true

Hide title:
?hideTitle=true

Custom button:
?submitLabel=Send%20Recording

All combined:
?hideWaveform=true&hideTitle=true&submitLabel=Complete

Pre-select recording type:
?preselect=video

Pre-fill name:
?name=John%20Doe

Pre-fill email:
?email=john.doe@example.com

Pre-fill folder ID:
?folderId=123456

Pre-fill custom question answers:
?field1=Answer%201&field2=Answer%202&field3=Answer%203

🔄 PostMessage API Reference

Commands (Parent → Iframe)

// Start recording
{
  action: 'start',
  timestamp: Date.now()  // optional
}

// Stop recording
{
  action: 'stop',
  timestamp: Date.now()  // optional
}

Responses (Iframe → Parent)

// Success
{
  source: 'speak-embed-recorder',
  status: 'success',
  message: 'Recording started',
  timestamp: '2025-10-09T10:30:00.000Z',
  data: { action: 'start' }
}

// Error
{
  source: 'speak-embed-recorder',
  status: 'error',
  message: 'Recording already in progress',
  timestamp: '2025-10-09T10:30:00.000Z'
}

💡 Common Patterns

Pattern 1: Minimal UI

<iframe
  src="https://recorder.speakai.co/iframe/TOKEN?hideWaveform=true&hideTitle=true"
  allow="microphone; camera"
  style="width: 100%; height: 500px; border: none;">
</iframe>

Pattern 2: Custom Branding

<iframe
  src="https://recorder.speakai.co/iframe/TOKEN?submitLabel=Submit%20to%20Support"
  allow="microphone; camera">
</iframe>

Pattern 3: External Controls

<div class="recording-controls">
  <button onclick="startRecording()">🔴 Start</button>
  <button onclick="stopRecording()">⏹ Stop</button>
  <div id="status">Ready</div>
</div>

<iframe id="recorder" src="https://recorder.speakai.co/iframe/TOKEN"></iframe>

<script>
const iframe = document.getElementById('recorder');
const status = document.getElementById('status');

function startRecording() {
  iframe.contentWindow.postMessage({action: 'start'}, '*');
  status.textContent = 'Recording...';
}

function stopRecording() {
  iframe.contentWindow.postMessage({action: 'stop'}, '*');
  status.textContent = 'Stopped';
}

window.addEventListener('message', (e) => {
  const response = JSON.parse(e.data);
  if (response.status === 'error') {
    status.textContent = 'Error: ' + response.message;
  }
});
</script>

⚙️ Troubleshooting

Iframe Not Loading

const iframe = document.querySelector('iframe');
console.log('Iframe loaded:', iframe.contentWindow !== null);

iframe.addEventListener('load', () => {
  console.log('Iframe loaded successfully');
});

PostMessage Not Working

const iframe = document.querySelector('iframe');

console.log('Iframe found:', iframe !== null);
console.log('ContentWindow:', iframe.contentWindow);

function sendDebugMessage(action) {
  console.log('Sending:', action);
  iframe.contentWindow.postMessage({ action: action }, '*');
  console.log('Message sent');
}

sendDebugMessage('start');

Parameters Not Applied

const iframe = document.querySelector('iframe');
console.log('Iframe src:', iframe.src);

const url = new URL(iframe.src);
console.log('hideWaveform:', url.searchParams.get('hideWaveform'));
console.log('hideTitle:', url.searchParams.get('hideTitle'));
console.log('submitLabel:', url.searchParams.get('submitLabel'));

⚛️ Framework Examples

React

import { useEffect, useRef } from 'react';

function RecorderEmbed({ token }) {
  const iframeRef = useRef(null);

  useEffect(() => {
    const handleMessage = (event) => {
      if (event.origin !== 'https://recorder.speakai.co') return;
      const response = JSON.parse(event.data);
      console.log('Recorder:', response);
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  const startRecording = () => {
    iframeRef.current?.contentWindow.postMessage({ action: 'start' }, 'https://recorder.speakai.co');
  };

  const stopRecording = () => {
    iframeRef.current?.contentWindow.postMessage({ action: 'stop' }, 'https://recorder.speakai.co');
  };

  return (
    <div>
      <div>
        <button onClick={startRecording}>Start</button>
        <button onClick={stopRecording}>Stop</button>
      </div>
      <iframe
        ref={iframeRef}
        src={`https://recorder.speakai.co/iframe/${token}?hideWaveform=true`}
        allow="microphone; camera"
        style=#{{ width: '100%', height: '700px', border: 'none' }}
      />
    </div>
  );
}

Vue

<template>
  <div>
    <div>
      <button @click="startRecording">Start</button>
      <button @click="stopRecording">Stop</button>
    </div>
    <iframe
      ref="recorder"
      :src="iframeUrl"
      allow="microphone; camera"
      style="width: 100%; height: 700px; border: none"
    />
  </div>
</template>

<script>
export default {
  props: ['token'],
  computed: {
    iframeUrl() {
      return `https://recorder.speakai.co/iframe/${this.token}?hideWaveform=true`;
    }
  },
  mounted() {
    window.addEventListener('message', this.handleMessage);
  },
  beforeUnmount() {
    window.removeEventListener('message', this.handleMessage);
  },
  methods: {
    handleMessage(event) {
      if (event.origin !== 'https://recorder.speakai.co') return;
      const response = JSON.parse(event.data);
      console.log('Recorder:', response);
    },
    startRecording() {
      this.$refs.recorder.contentWindow.postMessage(
        { action: 'start' },
        'https://recorder.speakai.co'
      );
    },
    stopRecording() {
      this.$refs.recorder.contentWindow.postMessage(
        { action: 'stop' },
        'https://recorder.speakai.co'
      );
    }
  }
}
</script>

Angular

import { Component, ElementRef, ViewChild, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-recorder',
  template: `
    <div>
      <button (click)="startRecording()">Start</button>
      <button (click)="stopRecording()">Stop</button>
    </div>
    <iframe
      #recorder
      [src]="iframeUrl"
      allow="microphone; camera"
      style="width: 100%; height: 700px; border: none">
    </iframe>
  `
})
export class RecorderComponent implements OnInit, OnDestroy {
  @ViewChild('recorder') iframeElement: ElementRef;
  iframeUrl = 'https://recorder.speakai.co/iframe/TOKEN?hideWaveform=true';

  ngOnInit() {
    window.addEventListener('message', this.handleMessage);
  }

  ngOnDestroy() {
    window.removeEventListener('message', this.handleMessage);
  }

  handleMessage = (event: MessageEvent) => {
    if (event.origin !== 'https://recorder.speakai.co') return;
    const response = JSON.parse(event.data);
    console.log('Recorder:', response);
  };

  startRecording() {
    const iframe = this.iframeElement.nativeElement as HTMLIFrameElement;
    iframe.contentWindow.postMessage({ action: 'start' }, 'https://recorder.speakai.co');
  }

  stopRecording() {
    const iframe = this.iframeElement.nativeElement as HTMLIFrameElement;
    iframe.contentWindow.postMessage({ action: 'stop' }, 'https://recorder.speakai.co');
  }
}

Support

For issues or questions:

  1. Check the troubleshooting section above
  2. Review the full test plan document
  3. Use the test embedder page to debug
  4. Check the browser console for error messages
  5. Contact us at success@speakai.co

New to Speak AI? Create a Speak AI account and work through Getting Started.

Iframe controls

5-Minute Quick Start, Speak AI Recorder Embed


Test Embed Iframe

Step 1: Basic Iframe Embedding

<!-- Minimal Embedding -->
<iframe
  src="https://recorder.speakai.co/iframe/YOUR_TOKEN_HERE"
  allow="microphone; camera"
  width="100%"
  height="700px">
</iframe>

Step 2: Add Query Parameters

<!-- With Query Parameters -->
<iframe
  src="https://recorder.speakai.co/iframe/YOUR_TOKEN_HERE?hideWaveform=true&hideTitle=true&submitLabel=Send"
  allow="microphone; camera"
  width="100%"
  height="700px">
</iframe>

Step 3: Add PostMessage Control

// Get iframe reference
const iframe = document.querySelector('iframe');

// Start recording
iframe.contentWindow.postMessage({ action: 'start' }, 'https://recorder.speakai.co');

// Stop recording
iframe.contentWindow.postMessage({ action: 'stop' }, 'https://recorder.speakai.co');

// Listen for responses
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://recorder.speakai.co') return;
  const response = JSON.parse(event.data);
  console.log('Response:', response);
});

⚙️ Query Parameters Reference

Parameter Type Default Description
hideWaveform boolean false Hide audio waveform visualization
hideTitle boolean false Hide title/header text
submitLabel string “Upload” Custom submit button text
hideSubmit boolean false Hide submit button
preselect string “audio” | “video” | “upload” | “screenshare” Pre-select recording type
name string “” Pre-fill name field
email string “” Pre-fill email field
folderId string “” Pre-fill folder ID
field1 - field10 string “” Pre-fill custom question answers (up to 10)

Examples

Hide waveform:
?hideWaveform=true

Hide title:
?hideTitle=true

Custom button:
?submitLabel=Send%20Recording

All combined:
?hideWaveform=true&hideTitle=true&submitLabel=Complete

Pre-select recording type:
?preselect=video

Pre-fill name:
?name=John%20Doe

Pre-fill email:
?email=john.doe@example.com

Pre-fill folder ID:
?folderId=123456

Pre-fill custom question answers:
?field1=Answer%201&field2=Answer%202&field3=Answer%203

🔄 PostMessage API Reference

Commands (Parent → Iframe)

// Start recording
{
  action: 'start',
  timestamp: Date.now()  // optional
}

// Stop recording
{
  action: 'stop',
  timestamp: Date.now()  // optional
}

Responses (Iframe → Parent)

// Success
{
  source: 'speak-embed-recorder',
  status: 'success',
  message: 'Recording started',
  timestamp: '2025-10-09T10:30:00.000Z',
  data: { action: 'start' }
}

// Error
{
  source: 'speak-embed-recorder',
  status: 'error',
  message: 'Recording already in progress',
  timestamp: '2025-10-09T10:30:00.000Z'
}

💡 Common Patterns

Pattern 1: Minimal UI

<iframe
  src="https://recorder.speakai.co/iframe/TOKEN?hideWaveform=true&hideTitle=true"
  allow="microphone; camera"
  style="width: 100%; height: 500px; border: none;">
</iframe>

Pattern 2: Custom Branding

<iframe
  src="https://recorder.speakai.co/iframe/TOKEN?submitLabel=Submit%20to%20Support"
  allow="microphone; camera">
</iframe>

Pattern 3: External Controls

<div class="recording-controls">
  <button onclick="startRecording()">🔴 Start</button>
  <button onclick="stopRecording()">⏹ Stop</button>
  <div id="status">Ready</div>
</div>

<iframe id="recorder" src="https://recorder.speakai.co/iframe/TOKEN"></iframe>

<script>
const iframe = document.getElementById('recorder');
const status = document.getElementById('status');

function startRecording() {
  iframe.contentWindow.postMessage({action: 'start'}, '*');
  status.textContent = 'Recording...';
}

function stopRecording() {
  iframe.contentWindow.postMessage({action: 'stop'}, '*');
  status.textContent = 'Stopped';
}

window.addEventListener('message', (e) => {
  const response = JSON.parse(e.data);
  if (response.status === 'error') {
    status.textContent = 'Error: ' + response.message;
  }
});
</script>

⚙️ Troubleshooting

Iframe Not Loading

const iframe = document.querySelector('iframe');
console.log('Iframe loaded:', iframe.contentWindow !== null);

iframe.addEventListener('load', () => {
  console.log('Iframe loaded successfully');
});

PostMessage Not Working

const iframe = document.querySelector('iframe');

console.log('Iframe found:', iframe !== null);
console.log('ContentWindow:', iframe.contentWindow);

function sendDebugMessage(action) {
  console.log('Sending:', action);
  iframe.contentWindow.postMessage({ action: action }, '*');
  console.log('Message sent');
}

sendDebugMessage('start');

Parameters Not Applied

const iframe = document.querySelector('iframe');
console.log('Iframe src:', iframe.src);

const url = new URL(iframe.src);
console.log('hideWaveform:', url.searchParams.get('hideWaveform'));
console.log('hideTitle:', url.searchParams.get('hideTitle'));
console.log('submitLabel:', url.searchParams.get('submitLabel'));

⚛️ Framework Examples

React

import { useEffect, useRef } from 'react';

function RecorderEmbed({ token }) {
  const iframeRef = useRef(null);

  useEffect(() => {
    const handleMessage = (event) => {
      if (event.origin !== 'https://recorder.speakai.co') return;
      const response = JSON.parse(event.data);
      console.log('Recorder:', response);
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  const startRecording = () => {
    iframeRef.current?.contentWindow.postMessage({ action: 'start' }, 'https://recorder.speakai.co');
  };

  const stopRecording = () => {
    iframeRef.current?.contentWindow.postMessage({ action: 'stop' }, 'https://recorder.speakai.co');
  };

  return (
    <div>
      <div>
        <button onClick={startRecording}>Start</button>
        <button onClick={stopRecording}>Stop</button>
      </div>
      <iframe
        ref={iframeRef}
        src={`https://recorder.speakai.co/iframe/${token}?hideWaveform=true`}
        allow="microphone; camera"
        style=#{{ width: '100%', height: '700px', border: 'none' }}
      />
    </div>
  );
}

Vue

<template>
  <div>
    <div>
      <button @click="startRecording">Start</button>
      <button @click="stopRecording">Stop</button>
    </div>
    <iframe
      ref="recorder"
      :src="iframeUrl"
      allow="microphone; camera"
      style="width: 100%; height: 700px; border: none"
    />
  </div>
</template>

<script>
export default {
  props: ['token'],
  computed: {
    iframeUrl() {
      return `https://recorder.speakai.co/iframe/${this.token}?hideWaveform=true`;
    }
  },
  mounted() {
    window.addEventListener('message', this.handleMessage);
  },
  beforeUnmount() {
    window.removeEventListener('message', this.handleMessage);
  },
  methods: {
    handleMessage(event) {
      if (event.origin !== 'https://recorder.speakai.co') return;
      const response = JSON.parse(event.data);
      console.log('Recorder:', response);
    },
    startRecording() {
      this.$refs.recorder.contentWindow.postMessage(
        { action: 'start' },
        'https://recorder.speakai.co'
      );
    },
    stopRecording() {
      this.$refs.recorder.contentWindow.postMessage(
        { action: 'stop' },
        'https://recorder.speakai.co'
      );
    }
  }
}
</script>

Angular

import { Component, ElementRef, ViewChild, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-recorder',
  template: `
    <div>
      <button (click)="startRecording()">Start</button>
      <button (click)="stopRecording()">Stop</button>
    </div>
    <iframe
      #recorder
      [src]="iframeUrl"
      allow="microphone; camera"
      style="width: 100%; height: 700px; border: none">
    </iframe>
  `
})
export class RecorderComponent implements OnInit, OnDestroy {
  @ViewChild('recorder') iframeElement: ElementRef;
  iframeUrl = 'https://recorder.speakai.co/iframe/TOKEN?hideWaveform=true';

  ngOnInit() {
    window.addEventListener('message', this.handleMessage);
  }

  ngOnDestroy() {
    window.removeEventListener('message', this.handleMessage);
  }

  handleMessage = (event: MessageEvent) => {
    if (event.origin !== 'https://recorder.speakai.co') return;
    const response = JSON.parse(event.data);
    console.log('Recorder:', response);
  };

  startRecording() {
    const iframe = this.iframeElement.nativeElement as HTMLIFrameElement;
    iframe.contentWindow.postMessage({ action: 'start' }, 'https://recorder.speakai.co');
  }

  stopRecording() {
    const iframe = this.iframeElement.nativeElement as HTMLIFrameElement;
    iframe.contentWindow.postMessage({ action: 'stop' }, 'https://recorder.speakai.co');
  }
}

Support

For issues or questions:

  1. Check the troubleshooting section above
  2. Review the full test plan document
  3. Use the test embedder page to debug
  4. Check the browser console for error messages
  5. Contact us at success@speakai.co

New to Speak AI? Create a Speak AI account and work through Getting Started.

Evaluating Speak AI for a team? Book a demo and see your own recordings analyzed.


Related: Embeddable recorder · Browser support

Navigation

Type to search…

↑↓ navigate↵ selectEsc close