> ## Documentation Index
> Fetch the complete documentation index at: https://help.revenuehero.io/llms.txt
> Use this file to discover all available pages before exploring further.

# How to install the RevenueHero script?

> All the ways you can invoke the RevenueHero scheduler on your website, from standard form listeners to programmatic triggers like hero.submit() and hero.dialog.open().

The standard way to invoke RevenueHero is `hero.schedule('#form-id')`, which listens for your form's submit event and opens the scheduler automatically. But that doesn't always fit. Maybe you're running server-side validation before showing the calendar. Maybe your form lives inside an iframe. Maybe you're building in React and there's no traditional form submit event to listen to.

This article covers every way to invoke the RevenueHero scheduler on your website. For the step-by-step router setup that generates your script snippet, see [Create Inbound Router](/routers/inbound/create-inbound-router#install-the-script).

<Note>
  **BEFORE YOU BEGIN**

  1. [Create an Inbound Router](/routers/inbound/create-inbound-router) and note your **Router ID**
  2. Add the RevenueHero source script to the `<head>` of your page:

  ```html theme={null}
  <script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>
  ```
</Note>

## Methods

The RevenueHero script exposes three methods. The first, `hero.schedule()`, is the standard method documented in every [web forms integration guide](/web-forms/custom). The other two, `hero.submit()` and `hero.dialog.open()`, give you programmatic control over the entire flow.

### hero.schedule(formSelector)

Binds to a form's native submit event. When the form is submitted, RevenueHero intercepts the data, runs routing rules, and opens the scheduler automatically.

```javascript theme={null}
const hero = new RevenueHero({ routerId: 'xxx' });
hero.schedule('#demo-form'); // CSS selector for your form element
```

**When to use:** Your form uses a standard HTML submit and you don't need to intercept the submission before the scheduler appears.

**Limitation:** If you call `e.preventDefault()` on the form's submit event before RevenueHero can listen to it, `hero.schedule()` will never fire. Use `hero.submit()` instead.

### hero.submit(formData)

Sends form field data directly to RevenueHero for routing and qualification. Returns a Promise that resolves with session data you can use to open the scheduler.

```javascript theme={null}
const hero = new RevenueHero({ routerId: 'xxx' });

hero.submit({
  email: 'jane@acme.com',
  firstname: 'Jane',
  lastname: 'Doe',
  company: 'Acme Inc'
}).then(function(sessionData) {
  hero.dialog.open(sessionData);
});
```

**When to use:** You need to control what happens between form submission and the scheduler appearing. Custom validation, async API calls, multi-step forms, SPA frameworks, or any flow where `hero.schedule()` doesn't work.

<Warning>
  **Pass a plain object, not a FormData instance.** If you're collecting data from a form element, convert it first:

  ```javascript theme={null}
  const formElement = document.getElementById('demo-form');
  const formData = Object.fromEntries(new FormData(formElement).entries());
  hero.submit(formData); // plain object like { email: '...', name: '...' }
  ```

  Passing a raw `FormData` object will silently fail.
</Warning>

### hero.dialog.open(sessionData)

Opens the scheduler popup programmatically. Takes the session data returned from `hero.submit()`.

```javascript theme={null}
hero.submit(formData).then(function(sessionData) {
  hero.dialog.open(sessionData);
});
```

**When to use:** Always used together with `hero.submit()`. This is what actually makes the calendar visible to your prospect.

## Constructor options

The `RevenueHero` constructor accepts a configuration object:

| Parameter    | Type    | Required | Description                                               |
| ------------ | ------- | -------- | --------------------------------------------------------- |
| `routerId`   | string  | Yes      | Your Router ID from the Inbound Router setup              |
| `formType`   | string  | No       | Set to `'pardot'` or `'jotform'` for those form providers |
| `showLoader` | boolean | No       | Shows a loading spinner while routing rules are evaluated |

```javascript theme={null}
const hero = new RevenueHero({
  routerId: 'xxx',
  showLoader: true
});
```

Your Router ID is displayed in the **Widget Installation** step when you [create or edit an Inbound Router](/routers/inbound/create-inbound-router#install-the-script).

## Custom validation before scheduling

The most common reason to use `hero.submit()` is when you need to validate form data before showing the scheduler. Run your validation, and only trigger RevenueHero if it passes.

```html theme={null}
<form id="bookademoform">
  <input name="email" type="email" required />
  <input name="firstName" type="text" required />
  <input name="company" type="text" />
  <button type="submit">Request Demo</button>
</form>

<script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>
<script type="text/javascript">
  const form = document.getElementById('bookademoform');

  form.addEventListener('submit', async function(e) {
    e.preventDefault();

    const data = Object.fromEntries(new FormData(form).entries());

    // Your custom validation (API call, domain check, etc.)
    const response = await fetch('https://your-api.com/validate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: data.email })
    });
    const result = await response.json();

    if (result.status === 'valid') {
      // Only show the scheduler for valid leads
      const hero = new RevenueHero({ routerId: 'xxx', showLoader: true });
      hero.submit(data).then(function(sessionData) {
        hero.dialog.open(sessionData);
      });
    } else {
      // Show an error message
      document.getElementById('error').textContent = 'Please use a work email.';
    }
  });
</script>
```

<Tip>
  This pattern is useful for running checks like email validation services, domain blocklists, or CRM lookups before consuming a RevenueHero routing. If the check fails, the prospect never sees the scheduler and no routing log entry is created.
</Tip>

## Webflow forms (AJAX intercept)

Webflow forms submit via AJAX by default. If the standard `hero.schedule()` doesn't trigger the scheduler on your Webflow site, use this jQuery-based intercept pattern:

```html theme={null}
<script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>
<script type="text/javascript">
  jQuery(document).ajaxComplete(function(event, xhr, settings) {
    if (settings.url.includes('https://webflow.com/api/v1/form/')) {
      var formElement = document.querySelector('#wf-form-Email-Form');
      var isSuccessful = xhr.status === 200;
      var isTargetForm = settings.data.includes(formElement.dataset['wfElementId']);

      if (isTargetForm && isSuccessful) {
        var submitData = Object.fromEntries(new FormData(formElement).entries());
        var hero = new RevenueHero({ routerId: 'xxx' });
        hero.submit(submitData).then(function(sessionData) {
          hero.dialog.open(sessionData);
        });
      }
    }
  });
</script>
```

Replace `#wf-form-Email-Form` with your Webflow form's CSS ID and `'xxx'` with your Router ID.

<Warning>
  This script must be placed in the **Before `</body>` tag** section in Webflow, not the `<head>`. The form element needs to exist in the DOM before the script runs.
</Warning>

For the standard Webflow installation that works without this intercept, see [Webflow Forms](/web-forms/webflow).

## React and Next.js

In single-page applications, the RevenueHero script needs to load dynamically since there's no traditional page load.

### React

```jsx theme={null}
import { useEffect, useRef } from 'react';

function DemoForm() {
  const formRef = useRef(null);

  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://assets.revenuehero.io/scheduler.min.js';
    script.onload = () => {
      // Script loaded, RevenueHero is now available on window.RevenueHero
    };
    document.head.appendChild(script);

    return () => {
      document.head.removeChild(script);
    };
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(formRef.current).entries());

    const hero = new window.RevenueHero({ routerId: 'xxx', showLoader: true });
    const sessionData = await hero.submit(data);
    hero.dialog.open(sessionData);
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="firstname" type="text" required />
      <button type="submit">Book a Demo</button>
    </form>
  );
}
```

### Next.js

Use the Next.js `Script` component to load the RevenueHero script:

```jsx theme={null}
import Script from 'next/script';

export default function DemoPage() {
  const handleSubmit = async (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target).entries());

    const hero = new window.RevenueHero({ routerId: 'xxx', showLoader: true });
    const sessionData = await hero.submit(data);
    hero.dialog.open(sessionData);
  };

  return (
    <>
      <Script
        src="https://assets.revenuehero.io/scheduler.min.js"
        strategy="afterInteractive"
      />
      <form onSubmit={handleSubmit}>
        <input name="email" type="email" required />
        <input name="firstname" type="text" required />
        <button type="submit">Book a Demo</button>
      </form>
    </>
  );
}
```

### Gatsby

Gatsby uses the `<Script>` component from `gatsby-script`:

```jsx theme={null}
import { Script } from 'gatsby';

function DemoForm() {
  const handleSubmit = async (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target).entries());

    const hero = new window.RevenueHero({ routerId: 'xxx' });
    const sessionData = await hero.submit(data);
    hero.dialog.open(sessionData);
  };

  return (
    <>
      <Script src="https://assets.revenuehero.io/scheduler.min.js" />
      <form onSubmit={handleSubmit}>
        <input name="email" type="email" required />
        <input name="firstname" type="text" required />
        <button type="submit">Book a Demo</button>
      </form>
    </>
  );
}
```

## Google Tag Manager

You can load the RevenueHero script through GTM instead of adding it directly to your page HTML.

### Loading the script via GTM

1. In Google Tag Manager, create a new **Custom HTML** tag
2. Add the script:
   ```html theme={null}
   <script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>
   <script type="text/javascript">
     window.hero = new RevenueHero({ routerId: 'xxxx' });
     hero.schedule('#demo-form');
   </script>
   ```
3. Set the trigger to **DOM Ready** on the pages where your form exists
4. Publish the container

<Warning>
  Do **not** use the "All Pages" trigger with a Page View firing option. The script needs the form element to be present in the DOM when it executes. Use **DOM Ready** or **Window Loaded** as the trigger.
</Warning>

### Tracking conversions with GTM

Use RevenueHero's [JavaScript Events](/integrations/javascript-events) to push conversion data into the GTM data layer:

```html theme={null}
<script type="text/javascript">
  window.addEventListener('message', function(ev) {
    if (ev.data.type === 'PAGE_LOADED') {
      // Fires when the scheduler is shown (lead qualified)
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({ event: 'rh_qualified' });
    }

    if (ev.data.type === 'MEETING_BOOKED') {
      // Fires when a meeting is booked
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: 'rh_meeting_booked',
        booker_email: ev.data.meeting.attributes.booker_email,
        meeting_time: ev.data.meeting.attributes.meeting_time
      });
    }
  });
</script>
```

The `PAGE_LOADED` event is particularly useful for Google Ads conversion tracking. It fires when the scheduler with booking slots is displayed, which means the lead passed your routing qualification. Use this as a "Qualified Lead" conversion action.

## Global script across multiple pages

If you use the same form structure across many pages (e.g., the same HubSpot form on 400+ landing pages), you can install one script at the template level.

Add the RevenueHero script to your site's global template or theme footer. The script will look for the specified form selector on every page. If the form isn't present on a particular page, the script does nothing.

```html theme={null}
<!-- Add to your site-wide template, theme footer, or CMS global code -->
<script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>
<script type="text/javascript">
  window.hero = new RevenueHero({ routerId: 'xxx' });
  hero.schedule('#demo-form');
</script>
```

<Tip>
  If you have multiple forms mapped to different routers, use a single `scheduler.min.js` include in the `<head>` and separate `hero.schedule()` calls in the `<body>` of each page.
</Tip>

## Re-trigger the scheduler (booking incomplete page)

If a prospect fills your form but doesn't book a meeting, you can create a "Booking Incomplete" page that lets them try again. Store the form data in `localStorage` after the initial submission, then use `hero.submit()` on the retry page.

### On your original form page

```javascript theme={null}
form.addEventListener('submit', function(e) {
  // Save the form data for the retry page
  var data = Object.fromEntries(new FormData(form).entries());
  localStorage.setItem('rhdata', JSON.stringify(data));
  localStorage.setItem('rhid', 'xxx'); // your Router ID
});
```

### On the "booking incomplete" page

```html theme={null}
<button id="book-now">Book a Time</button>

<script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>
<script type="text/javascript">
  document.getElementById('book-now').addEventListener('click', function() {
    var savedData = localStorage.getItem('rhdata');
    var routerId = localStorage.getItem('rhid');

    if (savedData && routerId) {
      var hero = new RevenueHero({ routerId: routerId, showLoader: true });
      hero.submit(JSON.parse(savedData)).then(function(sessionData) {
        hero.dialog.open(sessionData);
      });
    }
  });
</script>
```

## Troubleshooting

### Scheduler doesn't appear after form submit

**Check the form selector.** The CSS selector passed to `hero.schedule()` must match your form element exactly. Open your browser's developer console and run `document.querySelector('#your-form-id')` to verify the form element exists.

**Check for `e.preventDefault()`.** If your JavaScript calls `e.preventDefault()` on the form's submit event before RevenueHero's listener fires, the scheduler won't trigger. Switch to `hero.submit()` + `hero.dialog.open()`.

**Check the browser console.** Look for errors from `scheduler.min.js`. A 500 error from the RevenueHero API usually means a field mapping mismatch. Verify that the field names in your form match the mappings in your Inbound Router.

### Script not loading

**Don't defer the script.** Adding `defer` or `async` attributes to the `scheduler.min.js` script tag can cause it to load after your form's submit handler runs. Load it synchronously in the `<head>` section.

```html theme={null}
<!-- Correct -->
<script type="text/javascript" src="https://assets.revenuehero.io/scheduler.min.js"></script>

<!-- Avoid -->
<script defer src="https://assets.revenuehero.io/scheduler.min.js"></script>
```

**WordPress / WP Rocket users:** WP Rocket and similar optimization plugins may automatically defer or lazy-load third-party scripts. Exclude `scheduler.min.js` from defer, delay, and minification settings.

### Form inside an iframe

If your form renders inside an iframe (common with Pardot embedded forms), the parent page's RevenueHero script cannot access the form's submit event due to cross-origin restrictions.

**Solutions:**

1. Use the [Pardot integration](/web-forms/pardot) which handles this natively with `formType: 'pardot'`
2. Add the RevenueHero script inside the iframe's HTML (in the form's "Below Form" or "Thank You" content area)
3. If neither works, contact support. The team can build a custom script for your form handler setup.

### PHP or CMS wrapping the script in HTML tags

Some CMS platforms (WordPress visual editors, PHP templates) may wrap your `<script>` tag in `<p>` or `<div>` tags, which breaks execution. Use your CMS's raw HTML or code block to prevent this.

***

Your advanced script installation is configured. Once you've added the script to your page, submit a test form entry and check your [Routing Logs](/reports/inbound/routing-logs) to confirm submissions are flowing through. 🎉🎉🎉

<CardGroup cols={2}>
  <Card title="JavaScript Events" icon="code" iconType="solid" href="/integrations/javascript-events">
    Listen for MEETING\_BOOKED, PAGE\_LOADED, and other events from the scheduler widget.
  </Card>

  <Card title="Create Inbound Router" icon="route" iconType="solid" href="/routers/inbound/create-inbound-router">
    Set up routing rules and get your Router ID.
  </Card>

  <Card title="Mapping Your Form" icon="table" iconType="solid" href="/routers/inbound/mapping-your-form">
    Map your form fields so RevenueHero can use them in routing conditions.
  </Card>

  <Card title="Custom HTML Forms" icon="file-code" iconType="solid" href="/web-forms/custom">
    Standard script installation for custom HTML forms.
  </Card>
</CardGroup>
