> 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/b-embed-form.md).

# Quickstart B — Embed a form & receive submissions

**What you'll build:** an InsurGrid intake form embedded directly in your app, plus a webhook endpoint that receives each completed submission — every answer, contact details, and links to any uploaded files, in one event.

**Time:** \~25 minutes.

**Prerequisites** (from [Get started](/get-started/01-what-you-can-build.md)):

* [ ] An InsurGrid account with access to the **API section** and at least one form (use a sandbox sample form if you haven't built one yet).
* [ ] Your **signing secret** (from [API credentials](/get-started/03-api-credentials.md)) — to verify the webhook.
* [ ] Node 18+ *or* Python 3.9+ installed.
* [ ] [ngrok](/get-started/05-local-development.md) (or any tunnel) if you're on your local machine.

***

## Step 1 — Get a form's `public_id`

Every form has a stable **`public_id`** (e.g. `f_9c2a7b1e4d`) — a public identifier used in the embed URL and echoed in every submission webhook. Find it in the dashboard's **Forms** section, or fetch it programmatically:

```bash
curl https://api.insurgrid.com/v2/forms \
  -H "Authorization: Bearer $INSURGRID_API_TOKEN"
```

The response lists each form's `id`, `public_id`, `name`, and `status`. Full details: Forms API reference.

## Step 2 — Drop in the iframe · *client*

Embed the form with a plain `<iframe>`. InsurGrid hosts and runs the form — the consumer fills it in place, with no redirect and no InsurGrid branding around it.

> **New term — `?embed=1`:** a flag on the form URL that renders the form in **embed mode**: InsurGrid's own header and page decoration are stripped, so only the form itself shows inside your frame.

```html
<iframe
  src="https://app.insurgrid.com/acme-agency/form/f_9c2a7b1e4d/apartment-intake-acme?embed=1"
  title="Apartment Intake"
  width="100%"
  height="800"
  style="border:0;"
  loading="lazy">
</iframe>
```

The URL shape is `https://app.insurgrid.com/{agentSlug}/form/{publicId}/{formSlug}?embed=1`. The `agentSlug` determines which agent the submission is attributed to — embed the same form under different agent slugs to attribute submissions per agent.

> ⚠️ **Tell InsurGrid which domain(s) you'll embed on.** Forms are served with a `frame-ancestors` content-security policy that only allows authorized origins to frame them — an un-listed domain gets a blank iframe, not an error you can see in your own console.

## Step 3 — (Optional) Size the frame · *client*

The embedded form posts **browser messages** to the parent window as the consumer interacts with it — so your page can size the frame or track progress.

> **New term — browser message (`postMessage`):** a way for the code inside an iframe to send data to the page that embeds it, without either page having direct access to the other's contents. The browser is the only thing that sees it.

Listen for `insurgrid:resize` and grow the iframe to fit the content:

```js
// on your page — run in the browser, not on a server
const iframe = document.querySelector("#insurgrid-form");

window.addEventListener("message", (e) => {
  if (e.origin !== "https://app.insurgrid.com") return;   // always verify origin
  const msg = e.data;
  if (msg?.type === "insurgrid:resize") {
    iframe.style.height = msg.height + "px";
  }
});
```

> ⚠️ **Always check `e.origin`.** Without that check, any page that knows how to `postMessage` to your window — not just the InsurGrid iframe — could trigger your handler.

Other available messages: `insurgrid:loaded` (form ready), `insurgrid:step` (`{ index, total }` as the consumer moves between steps), `insurgrid:completed` (`{ submission_id }` when they submit). Use these to drive your own progress UI or show a "thanks" screen — **never** as your data source. See Step 5.

## Step 4 — Stand up & verify your webhook · *server*

Submissions arrive at an HTTPS endpoint you control, the same way policy-data events do in [Quickstart A](/quickstarts/a-receive-policy-data.md) — set up the route, acknowledge with `200` immediately, and verify the `x-signature` header over the **raw request body** before trusting the payload. Reuse the exact server and signature-verification code from [Quickstart A, Steps 2–5](/quickstarts/a-receive-policy-data.md#step-2--stand-up-an-endpoint--server); the only differences here:

* Register the endpoint against the **`form-submission`** topic, not `client-submission`.
* The `topic` field in the payload body will read `"form-submission"`.

```js
// server.js — same server as Quickstart A, routed to form-submission
app.post("/hooks/insurgrid", (req, res) => {
  const ok = verify(req.rawBody, req.get("x-signature"), process.env.INSURGRID_SIGNING_SECRET);
  if (!ok) return res.sendStatus(401);
  res.sendStatus(200);
  handleSubmission(req.body.data);
});
```

If you're on your local machine, expose it the same way — [Expose your local endpoint](/get-started/05-local-development.md) — and add the tunnel URL as your staging `form-submission` webhook in the dashboard's **API section**.

## Step 5 — Read the payload · *server*

A verified `form-submission` webhook looks like this:

```json
{
  "topic": "form-submission",
  "data": {
    "form": {
      "id": 4412,
      "public_id": "f_9c2a7b1e4d",
      "name": "Apartment Intake — Acme Agency",
      "insurance_line": "commercial"
    },
    "submission_id": 55231,
    "trace_id": "tr_5f3c9a2b7e14",
    "submitted_at": "2026-08-24T18:22:40Z",

    "agent": { "id": 88213, "name": "Jane Smith", "email": "jane@acme-agency.com" },
    "contact": { "name": "Jane Roe", "email": "jane.roe@example.com", "phone": "(555) 010-1234" },

    "answers": [
      { "question_id": "b1c2-…", "label": "Full name", "type": "short_text", "value": "Jane Roe" },
      { "question_id": "d4e5-…", "label": "Loss runs", "type": "file_upload",
        "value": [ { "filename": "losses.pdf", "content_type": "application/pdf",
                     "url": "https://files.insurgrid.com/…" } ] }
    ],

    "acord": {
      "is_acord_ready": true,
      "form_ids": ["125", "140", "126"],
      "archive_url": "https://files.insurgrid.com/…/acord-archive.zip"
    },

    "metadata": { "partner_ref": "your-correlation-id" }
  }
}
```

| Field                | Type              | Meaning                                                                                                          | Example                                 |
| -------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `topic`              | string            | The event type. For form submissions it's `form-submission`.                                                     | `"form-submission"`                     |
| `data.form`          | object            | Which form was submitted — `id`, `public_id`, `name`, `insurance_line`.                                          | `{ "public_id": "f_9c2a7b1e4d", … }`    |
| `data.submission_id` | number            | Unique id for this submission. Your de-dupe key — see Step 6.                                                    | `55231`                                 |
| `data.trace_id`      | string            | Correlation id — quote it if you contact support.                                                                | `"tr_5f3c9a2b7e14"`                     |
| `data.submitted_at`  | string (ISO-8601) | When the consumer submitted, in UTC.                                                                             | `"2026-08-24T18:22:40Z"`                |
| `data.agent`         | object            | The InsurGrid agent this submission is attributed to.                                                            | `{ "id": 88213, "name": "Jane Smith" }` |
| `data.contact`       | object            | The consumer's contact details, when captured.                                                                   | `{ "email": "jane.roe@example.com" }`   |
| `data.answers[]`     | array             | Every answer — `question_id`, `label`, `type`, and the typed `value`. Shape varies by question `type`.           | see field reference below               |
| `data.acord`         | object            | Present when the form maps to ACORD forms. `archive_url` is a **time-limited** link to a zip of the filled PDFs. | `{ "is_acord_ready": true, … }`         |
| `data.metadata`      | object            | Any correlation values you supplied when the form link was generated, echoed back.                               | `{ "partner_ref": "…" }`                |

The full answer-value shape per question type (short text, address, file upload, data table, etc.) is in the Webhook events & payloads reference.

> This is the same underlying data you'd get from `GET /v2/submissions/{submission_id}` — see the Submissions API reference if you ever need to re-fetch a submission instead of waiting on the webhook.

## Step 6 — De-duplicate on `submission_id` · *server*

Delivery is **at-least-once** — the same event may occasionally arrive more than once. Use `data.submission_id` as your idempotency key and make processing it twice harmless:

```python
# server (sketch)
def handle_submission(data):
    sid = data["submission_id"]
    if already_processed(sid):
        return
    mark_processed(sid)
    save(data)
```

See Delivery & de-duplication for the full pattern.

## Step 7 — Download files before the links expire · *server*

Any `file_upload` answer carries `url`s that are **time-limited** download links.

> ⚠️ **File links expire.** Fetch and store each file on your side promptly when the webhook lands — don't save the `url` and expect it to work later. The same applies to `data.acord.archive_url`. If a link has already expired, re-fetch the submission via `GET /v2/submissions/{submission_id}` — reads are freshly signed every time.

## Step 8 — Try it out (sandbox)

1. Confirm your form is embedded and loads inside your page (`insurgrid:loaded` fires in the browser console).
2. In the **API section**, use **Send test event** to fire a sample `form-submission` at your staging webhook URL — no need to actually submit the embedded form.
3. Watch your server log the answers and file links.
4. Check the **delivery log** in the dashboard to confirm the attempt and your `200` response.

If nothing arrives, jump to Troubleshooting → "My webhook isn't arriving".

***

## Next steps

* [Build & manage forms via the API →](/intake-forms/c-build-forms-api.md)
* Webhook events & payloads (all fields) →
* The two embeds — builder vs fill form →
* Verify signatures — deeper dive →


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.insurgrid.com/intake-forms/b-embed-form.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
