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

The recorder runs inside an iframe, so you can put it on any page and keep visitors on your own site. This page covers the iframe itself, the query parameters that change what it shows, and the postMessage commands that let your own buttons drive it.

You can try any of it against a live recorder on the embed tester.

Add the iframe to your page

Paste the iframe where you want the recorder to appear, and replace YOUR_TOKEN_HERE with the token from the embed code on your recorder’s share step.

The Share tab of a recorder, on its Embed sub-tab. Static (iframe) is selected next to Script (advanced), with Width set to 100%, Height set to 600 and Preselect Recording Type set to None (User Selects). The Embed Code box below is masked here, above a Copy Embed Code button and a note about using your own branded domain instead of the default Speak AI one.

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

Keep the allow attribute. Without it the browser blocks the recorder from reaching the microphone and camera, and recording never starts. Give the frame enough height, around 700px, so your questions and the record button both fit without scrolling.

Site builders that strip the permissions

Site builders such as Wix and Webflow rewrite the iframes they render and drop the allow attribute. The recorder loads, but it cannot reach the microphone or camera in any browser. Add this script at the end of the body to put the permissions back on every iframe on the page, including iframes the builder injects after load.

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

Where the script goes in Wix

Go to Settings and scroll to Advanced, the last section, where you will find Custom Code. Add the script under the body - END option. Wix then asks whether to apply it to all pages or only specific ones.

Change what the recorder shows

Add query parameters to the iframe src to hide parts of the interface, preselect a recording type, or prefill answers.

<iframe
  src="https://recorder.speakai.co/iframe/YOUR_TOKEN_HERE?hideWaveform=true&hideTitle=true&submitLabel=Send"
  allow="microphone; camera"
  width="100%"
  height="700px">
</iframe>
Parameter Type Default Description
hideWaveform boolean false Hide the audio waveform
hideTitle boolean false Hide the title and header text
submitLabel string “Upload” Set the submit button text
hideSubmit boolean false Hide the submit button
preselect string none Preselect the recording type: audio, video, upload or screenshare
name string “” Prefill the name field
email string “” Prefill the email field
folderId string “” Prefill the folder ID
field1 to field10 string “” Prefill the answers to your custom questions, up to 10
isDownload boolean false Let the respondent download their own recording
redirectUrl string “” Send the respondent to this URL once the recording is submitted

The same parameters work on a shared recorder link, not only the iframe: put them after https://recorder.speakai.co/your-custom-url.

Join parameters with &, and URL encode any value that contains a space or a symbol.

?hideWaveform=true&hideTitle=true&submitLabel=Complete
?preselect=video
?name=John%20Doe&email=john.doe@example.com
?folderId=123456
?field1=Answer%201&field2=Answer%202

Start and stop recording from your own buttons

Send a command to the iframe with postMessage and listen for the reply on the parent window.

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

A command takes an action of start or stop, plus an optional timestamp. The recorder replies with a success or an error message, both stamped with source: 'speak-embed-recorder' so you can tell them apart from other traffic on the page.

// 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'
}

Put the two together and your page can carry its own controls while the recorder handles capture and upload.

<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" allow="microphone; camera"></iframe>

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

function startRecording() {
  iframe.contentWindow.postMessage({action: 'start'}, 'https://recorder.speakai.co');
  status.textContent = 'Recording...';
}

function stopRecording() {
  iframe.contentWindow.postMessage({action: 'stop'}, 'https://recorder.speakai.co');
  status.textContent = 'Stopped';
}

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

Always check event.origin before you trust a message, and send commands to https://recorder.speakai.co rather than *.

Embed in React, Vue or Angular

The pattern is the same in every framework: render the iframe, add the message listener when the component mounts, and remove it when the component unmounts.

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');
  }
}

If the embed does not work

Open the browser console first. Most problems show up there as a permissions error, a blocked frame, or a script that never ran.

The iframe does not load

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

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

postMessage does nothing

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 }, 'https://recorder.speakai.co');
  console.log('Message sent');
}

sendDebugMessage('start');

Parameters are ignored

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'));

If the recorder loads but the microphone stays silent, the permission is the usual cause. See Microphone and camera permissions. Still stuck? Send us the page URL at success@speakai.co.

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