> For the complete documentation index, see [llms.txt](https://docs.insurgrid.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.insurgrid.com/intake-forms/builder-sessions.md).

# Embed the form builder

Let your users build and edit their own intake forms, inside your product, without an InsurGrid account.

Your users need to change their intake forms — add a question, reword one, publish a new version. You could build that editor yourself, or you can drop InsurGrid's into a page in your product.

It appears inside your own interface, and each signed-in user sees only their own forms. They never create an InsurGrid account, never see a login screen, and never leave your product.

> **This is the builder, not the form itself.** The form your *customers* fill in is a different embed with no login at all — see [Quickstart B](/intake-forms/b-embed-form.md), or [The two embeds](/guides/embeds.md) if you're deciding between them.

***

## How it fits together

The builder needs to know which of your users is looking at it. It can't ask you directly — it runs in the browser, and your API key must never go there. So a short-lived token stands in.

```
  Your page  ── asks your server for a token ──▶  Your server
                                                       │
                                                       │  calls InsurGrid
                                                       │  with your API key
                                                       ▼
  Your page  ◀────── token ─────────────────  InsurGrid
      │
      └─▶ the embed script opens the builder with that token
```

The token is good for 15 minutes and for one user. Your API key stays on your server throughout.

That means there are three things to add: a **snippet** on the page, a **route** on your server, and your **key** in configuration.

***

## 1. Put your key in configuration

Server-side environment variables — never shipped to the browser.

```bash
# Staging
INSURGRID_API_KEY=your-staging-api-key
INSURGRID_API_URL=https://phoenix-staging.insurgrid.com/api/v1

# Production
INSURGRID_API_KEY=your-production-api-key
INSURGRID_API_URL=https://phoenix.insurgrid.com/api/v1
```

Create each key in that environment's dashboard, under **Settings → API keys** — [staging](https://dashboard-staging.insurgrid.com) or [production](https://dashboard.insurgrid.com). The key must belong to an agency admin.

## 2. Add a route that hands out tokens

One endpoint on your backend. It has three jobs: check the user is signed in, tell InsurGrid who they are, and pass the answer back.

```js
app.get('/insurgrid-token', requireLogin, async (req, res) => {
  const response = await fetch(`${process.env.INSURGRID_API_URL}/embed-sessions`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.INSURGRID_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ externalUserId: String(req.user.id) }),
  });
  res.set('Cache-Control', 'no-store').status(response.status).json(await response.json());
});
```

<details>

<summary>Python (Flask)</summary>

```python
@app.get("/insurgrid-token")
@login_required
def insurgrid_token():
    response = requests.post(
        f"{os.environ['INSURGRID_API_URL']}/embed-sessions",
        headers={"Authorization": f"Bearer {os.environ['INSURGRID_API_KEY']}"},
        json={"externalUserId": str(current_user.id)},
        timeout=10,
    )
    return response.json(), response.status_code, {"Cache-Control": "no-store"}
```

</details>

<details>

<summary>PHP</summary>

```php
// This route must run after your application's login middleware.
$response = $http->post(getenv('INSURGRID_API_URL') . '/embed-sessions', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('INSURGRID_API_KEY')],
    'json'    => ['externalUserId' => (string) $currentUser->id],
]);

return response($response->getBody(), $response->getStatusCode())
    ->header('Content-Type', 'application/json')
    ->header('Cache-Control', 'no-store');
```

</details>

InsurGrid replies with `201` and a token. Pass it back unchanged:

```json
{ "token": "emb_…", "expiresAt": "2026-09-30T12:15:00Z" }
```

Three things that matter here:

* **Take `externalUserId` from your login session**, never from a query string or request body. It decides whose forms appear.
* **Send `Cache-Control: no-store`**, so no browser or proxy keeps someone's token and serves it to the next person.
* **Keep the route behind your login.** It is the only thing standing between a visitor and a user's forms.

## 3. Add the snippet to the page

```html
<div id="insurgrid-forms"></div>
<script
  src="https://dashboard-staging.insurgrid.com/embed.js"
  data-token-url="/insurgrid-token">
</script>
```

Swap the host for `dashboard.insurgrid.com` in production. `data-token-url` is the route you just built.

> **The token route must be on the same origin as the page.** The script fetches it with same-origin credentials, which is how your login cookie reaches it — a route on another domain receives no cookie and can't tell who's asking. If your token endpoint has to live elsewhere, mount the embed yourself and fetch the token however you like; see [If your app is a single-page app](#if-your-app-is-a-single-page-app) below.

That's it. The builder appears, sized to its content, and refreshes its own token before each one expires.

### Adjusting it

| Attribute         | What it does                                                                  |
| ----------------- | ----------------------------------------------------------------------------- |
| `data-target`     | CSS selector for the container. Defaults to `#insurgrid-forms`.               |
| `data-view`       | `list` (default) shows their forms; `builder` opens the editor straight away. |
| `data-form-id`    | With `data-view="builder"`, which form to open.                               |
| `data-min-height` | Starting height in pixels. Defaults to 640.                                   |

***

## Reacting to what your users do

The embed raises ordinary browser events on the container, so you can refresh your own screen or record what happened. There's no `message` listener to write and no origin to check.

```js
document.querySelector('#insurgrid-forms')
  .addEventListener('insurgrid:form-published', (e) => {
    console.log(e.detail.formId, e.detail.shareUrl);
  });
```

| Event                      | Raised when                                     | `event.detail`         |
| -------------------------- | ----------------------------------------------- | ---------------------- |
| `insurgrid:ready`          | The builder has loaded and been handed a token. | —                      |
| `insurgrid:form-saved`     | They saved a draft.                             | `{ formId, shareUrl }` |
| `insurgrid:form-published` | They published a form.                          | `{ formId, shareUrl }` |
| `insurgrid:close`          | They backed out of the builder.                 | —                      |
| `insurgrid:error`          | Something went wrong.                           | `{ code }`             |

`insurgrid:ready` means the token has been handed over, not that it has been checked — a rejected token arrives moments later as `insurgrid:error`. If you hide a loading state on `ready`, handle `error` too.

## If your app is a single-page app

A React, Vue or Angular app — or any login that uses a bearer token rather than cookies, or a token endpoint on another origin — should mount the embed itself rather than relying on `data-token-url`:

```html
<script src="https://dashboard.insurgrid.com/embed.js"></script>
```

```js
const embed = InsurGrid.mount(document.querySelector('#insurgrid-forms'), {
  view: 'list',
  getToken: async () => {
    const response = await fetch('/insurgrid-token', {
      headers: { Authorization: `Bearer ${yourPortalToken}` },
    });
    if (!response.ok) throw new Error('Unable to start InsurGrid');
    return response.json();
  },
});

// when the component unmounts
embed.destroy();
```

`getToken` is yours — fetch from wherever you like, with whatever headers you need. Just return the `{ token, expiresAt }` InsurGrid gave your server, unchanged.

Call `destroy()` on unmount, or the embed keeps refreshing its token in the background.

***

## Going live

* [ ] It works end to end on staging — token route, builder loads, publishing succeeds.
* [ ] A production key exists and is stored on your server.
* [ ] `INSURGRID_API_URL` points at `https://phoenix.insurgrid.com/api/v1`.
* [ ] The script URL points at `https://dashboard.insurgrid.com/embed.js`.
* [ ] The token route is behind your login, and never returns your API key.

## Worth knowing

Your users can only ever see and edit their own forms. That comes from the token, which comes from your login session — nothing the browser sends can change it.

Carrier-connected forms, which log into a carrier to pull declaration pages, can't be created or switched on from the builder embed or the API. Those are built in the InsurGrid dashboard.

## Related

* [Forms API](/intake-forms/forms.md) — the same forms from your own code, with the full error and rate-limit reference.
* [The two embeds](/guides/embeds.md) — builder versus the form customers fill in.
* [Environments](/guides/environments.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.insurgrid.com/intake-forms/builder-sessions.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
