---
title: "Embedding"
description: "Add the Speak AI recorder to any page. Includes the fix for Wix and Webflow, which strip microphone and camera permissions."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.speakai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedding

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](https://recorder.speakai.co/assets/embed-tester.html).

## 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.](/help/media/recorder/recorder-embedding.jpg)

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

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

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

```text
?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.

```js
// 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.

```js
// 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.

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

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

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

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

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

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

### postMessage does nothing

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

```js
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](/help/recorder/permissions/). Still stuck? Send us the page URL at [success@speakai.co](mailto:success@speakai.co).

Evaluating Speak AI for a team? [Book a demo and see your own recordings analyzed](https://calendly.com/speak-ai/demo?utm_source=docs&utm_campaign=book-demo).

---
Related: [Embeddable recorder](/help/recorder/) · [Browser support](/help/recorder/browser-support/)

Source: https://docs.speakai.co/help/recorder/embedding/index.mdx
