> 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/forms.md).

# Forms API

Create, edit and publish intake forms from your own code, on behalf of your own users.

Intake forms collect what you need from a prospect before you can quote them. This API lets you build and manage those forms from your own application, so your users never have to leave your product to do it.

Each form belongs to one of **your** users. They see their own forms and nobody else's — you don't create InsurGrid accounts for them, and you don't manage permissions. You tell InsurGrid which of your users a form belongs to, and it handles the separation.

If you'd rather your users built forms visually than through code, the same forms are available in an embeddable builder — see [Embed the form builder](/intake-forms/builder-sessions.md). The two work on the same data.

> **Submissions don't come back through this API.** When someone completes a form, the submission is delivered to your webhook endpoint. See [Submissions](/intake-forms/submissions.md).

***

## Before you start

### Your API key

Every request carries your key as a bearer token:

```
Authorization: Bearer YOUR_API_KEY
```

Create and revoke keys in the dashboard under **Settings → API keys**. Two things to know:

* **The key must belong to an agency admin**, otherwise you get `403 admin_required`.
* **Forms access must be enabled** for the agent who owns the key. If it isn't, every call returns `403 feature_disabled` — contact us to switch it on.

Revoking a key stops the API and any live embeds on their next request. See [Get your API credentials](/get-started/03-api-credentials.md).

### Environments

|                      | Staging                                            | Production                                 |
| -------------------- | -------------------------------------------------- | ------------------------------------------ |
| API base URL         | `https://phoenix-staging.insurgrid.com/api/v1`     | `https://phoenix.insurgrid.com/api/v1`     |
| Embed script         | `https://dashboard-staging.insurgrid.com/embed.js` | `https://dashboard.insurgrid.com/embed.js` |
| Dashboard (API keys) | `https://dashboard-staging.insurgrid.com`          | `https://dashboard.insurgrid.com`          |

Build against staging, then change the host and the key. The two are fully separate: a staging key works only on staging, and a form created on staging doesn't exist on production. More in [Environments](/guides/environments.md).

### Which forms this API can see

`externalUserId` is how you tell InsurGrid whose form this is — a stable id from your own system, such as your internal user id. Any string up to 255 characters. You set it when you create a form; it can't be changed afterwards, and it's what decides who can see the form from then on.

A form is tied to two things: that `externalUserId`, and **the key that created it**. Both have to match for a form to be visible, which has two consequences worth planning around:

* **Forms built in the InsurGrid dashboard are invisible here.** They have no `externalUserId`, so they never appear in `GET /forms`, and `GET /forms/{id}` returns `404`. Only forms created through this API or the embedded builder are reachable.
* **Keys don't share forms, even inside one agency.** A form created with one admin's key returns `404` to another admin's key. Use one key consistently for a given integration, and don't expect a replacement key to inherit the forms the old one made — if you rotate, you'll be starting a new set.

***

## Quick start

**Create a draft:**

```bash
curl -X POST https://phoenix-staging.insurgrid.com/api/v1/forms \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "externalUserId": "user-8841", "name": "Home insurance intake" }'
```

Leave out `steps` and the form starts with a working default: a welcome step, a **contact info step** that already collects name, email and phone, and a confirmation step. The response carries the form's `id`.

**Publish it:**

```bash
curl -X PUT https://phoenix-staging.insurgrid.com/api/v1/forms/123/publish \
  -H "Authorization: Bearer YOUR_API_KEY"
```

The response now includes `shareUrl` — the public link you send to customers, or embed. For a walkthrough that goes further, see [Quickstart C](/intake-forms/c-build-forms-api.md).

***

## Endpoints

| Method   | Path                         | What it does                                   |
| -------- | ---------------------------- | ---------------------------------------------- |
| `GET`    | `/forms`                     | List forms.                                    |
| `POST`   | `/forms`                     | Create a draft form.                           |
| `GET`    | `/forms/{id}`                | Get one form with all its steps and questions. |
| `PATCH`  | `/forms/{id}`                | Update a form.                                 |
| `DELETE` | `/forms/{id}`                | Delete a form.                                 |
| `POST`   | `/forms/{id}/duplicate`      | Copy a form for the same user.                 |
| `PUT`    | `/forms/{id}/publish`        | Publish a form and get its `shareUrl`.         |
| `PUT`    | `/forms/{id}/archive`        | Take a form out of use.                        |
| `PUT`    | `/forms/{id}/reactivate`     | Make an archived form active again.            |
| `GET`    | `/forms/templates`           | Starter forms you can build from.              |
| `GET`    | `/forms/acord-catalog?line=` | ACORD forms and questions. `line` is required. |
| `POST`   | `/embed-sessions`            | A short-lived token for the embedded builder.  |

### Listing forms

```
GET /forms?externalUserId=user-8841&limit=50&offset=0
```

Everything is optional. Omit `externalUserId` to list across all of your users. `limit` runs 1–100 and defaults to 50.

Each entry carries:

| Field                                             | What it is                                                   |
| ------------------------------------------------- | ------------------------------------------------------------ |
| `id`                                              | The form's id, used in the paths above.                      |
| `public_id`                                       | The public identifier used in form links and embeds.         |
| `name`, `slug`                                    | The name, and the readable part of its web address.          |
| `status`                                          | `draft`, `active` or `archived`.                             |
| `externalUserId`                                  | Which of your users owns it.                                 |
| `shareUrl`                                        | Where to send people to fill it in — `null` until published. |
| `acord_numbers`, `policy_types`, `is_acord_ready` | ACORD mapping, if any.                                       |
| `created_at`, `updated_at`                        | Timestamps.                                                  |

`GET /forms/{id}` returns all of that plus the form itself: `steps`, `insurance_line`, `branding`, `redirect_url`, `auto_response_enabled`, `tracking` and `carrier_flow_enabled`.

### Creating a form

| Field            | Required | Notes                                                                                                                                                                   |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `externalUserId` | Yes      | Whose form this is.                                                                                                                                                     |
| `name`           | Yes      | Up to 255 characters.                                                                                                                                                   |
| `slug`           | No       | e.g. `home-intake`. Generated from `name` if omitted, with a short random suffix to keep it unique. Lower-case words separated by single hyphens, up to 100 characters. |
| `insurance_line` | No       | `personal` (default) or `commercial`.                                                                                                                                   |
| `steps`          | No       | The pages and questions. See below.                                                                                                                                     |
| `redirect_url`   | No       | Where to send the customer after they submit.                                                                                                                           |
| `branding`       | No       | Logo and colours.                                                                                                                                                       |

Forms are always created as drafts. Publishing is a separate call.

### Updating a form

`PATCH /forms/{id}` with only the fields you want to change. On update, unknown fields are rejected rather than ignored — a misspelled key is a `422`. (On create they're ignored, so a typo there passes silently.)

> ⚠️ **`steps` replaces rather than merges.** If you send it, send the complete list — any step you leave out is deleted, **including the default contact info step**. See [Keeping the contact block populated](#keeping-the-contact-block-populated).

### Statuses

| Status     | Meaning                               |
| ---------- | ------------------------------------- |
| `draft`    | Not live. `shareUrl` is `null`.       |
| `active`   | Published. `shareUrl` is set.         |
| `archived` | No longer live. `shareUrl` is `null`. |

***

## How a form is structured

A form is a list of **steps** — the pages someone moves through — and each step holds **questions**.

```json
{
  "externalUserId": "user-8841",
  "name": "Business intake",
  "steps": [
    {
      "type": "custom",
      "name": "About your business",
      "segment": "before",
      "position": 0,
      "questions": [
        { "type": "short_text", "label": "Business name", "required": true, "position": 0 },
        { "type": "dropdown", "label": "Number of employees", "position": 1,
          "options": [{ "label": "1-10" }, { "label": "11-50" }, { "label": "50+" }] }
      ]
    }
  ]
}
```

### Step fields

| Field       | Required | Notes                                                                                                                          |
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `type`      | Yes      | `welcome`, `contact_info`, `custom`, `confirmation` or `complete`. Only one of each non-`custom` type per form.                |
| `name`      | Yes      | Up to 255 characters.                                                                                                          |
| `segment`   | Yes      | Where the step sits in the flow: `before`, `carrier` or `after`. Easy to miss — it has no default, and omitting it is a `422`. |
| `position`  | Yes      | Order within the form, 0–1000.                                                                                                 |
| `enabled`   | No       | Defaults to `true`.                                                                                                            |
| `questions` | No       | The step's questions. Up to 200.                                                                                               |

A form must contain at least one `custom` or `contact_info` step — a form made only of welcome and confirmation pages collects nothing, and is rejected.

### Question fields

| Field              | Required             | Notes                                                                                                           |
| ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
| `type`             | Yes                  | One of the question types below.                                                                                |
| `label`            | Yes                  | The question text, up to 500 characters.                                                                        |
| `position`         | Yes                  | Order within the step, 0–1000.                                                                                  |
| `required`         | No                   | Defaults to `false`.                                                                                            |
| `placeholder`      | No                   | Up to 500 characters.                                                                                           |
| `field_mapping`    | No                   | Tags this question as carrying a known value. **This is what fills the webhook's `contact` block** — see below. |
| `options`          | For choice questions | Up to 500. Required for `dropdown`, `single_choice` and `multi_choice`.                                         |
| `initially_hidden` | No                   | Hide the question until a rule reveals it.                                                                      |
| `rules`            | No                   | Conditional logic, up to 100 per question.                                                                      |

**Step types:** `welcome`, `contact_info`, `custom`, `confirmation`, `complete`

**Question types:** `short_text`, `long_text`, `email`, `number`, `date`, `dropdown`, `single_choice`, `multi_choice`, `file_upload`, `address`, `data_table`

A form can hold up to 50 steps.

Rather than assembling this by hand, start from `GET /forms/templates`, or build a form the way you want it **in the embedded builder** and fetch it with `GET /forms/{id}` — what comes back is exactly what you can send. (Forms built in the InsurGrid dashboard won't work for this; see [Which forms this API can see](#which-forms-this-api-can-see).)

> **Carrier-connected forms** — the ones that log into a carrier to pull declaration pages — can't be created or enabled through this API. Build those in the dashboard.

### Keeping the contact block populated

Every submission webhook carries a `contact` object with the submitter's name, email and phone. InsurGrid fills it from questions tagged with `field_mapping` — **not** from a question's type, label or position.

| `field_mapping` | Fills           |
| --------------- | --------------- |
| `FullName`      | `contact.name`  |
| `Email`         | `contact.email` |
| `Phone`         | `contact.phone` |
| `ServiceType`   | —               |
| `EventDate`     | —               |

A form created without `steps` gets a contact info step whose three questions are already tagged, so `contact` works out of the box. **The moment you send your own `steps`, that step is gone** — along with its tags — and every webhook from then on arrives with:

```json
"contact": { "name": null, "email": null, "phone": null }
```

Nothing errors, and the answers are still there in `answers[]`. Only `contact` goes empty, so it is easy to miss until something downstream stops working.

So when you build your own structure, tag the equivalent questions:

```json
{ "type": "email", "label": "Your email", "required": true,
  "position": 1, "field_mapping": "Email" }
```

If two questions share a mapping, the first non-empty answer wins.

***

## Errors

Every error has the same shape:

```json
{ "error": { "code": "not_found", "message": "Form not found." } }
```

| Status | Code                     | What went wrong                                    |
| ------ | ------------------------ | -------------------------------------------------- |
| 400    | `invalid_request`        | The request is malformed.                          |
| 401    | `invalid_api_key`        | The key is missing, wrong or revoked.              |
| 401    | `invalid_embed_token`    | The embed token is missing or expired.             |
| 403    | `admin_required`         | The key doesn't belong to an agency admin.         |
| 403    | `feature_disabled`       | Forms aren't enabled for the key's agent.          |
| 403    | `scope_required`         | The key isn't scoped for this API.                 |
| 404    | `not_found`              | No such form, or it isn't visible to this key.     |
| 409    | `conflict`               | That slug is already taken.                        |
| 422    | `validation_error`       | A field is invalid — see `details`.                |
| 422    | `external_user_required` | `externalUserId` missing on create.                |
| 422    | `carrier_flow_disabled`  | Carrier steps aren't supported here.               |
| 429    | `rate_limited`           | Too many requests. Wait for `Retry-After` seconds. |
| 503    | `auth_unavailable`       | A temporary problem. Try again.                    |

## Rate limits

* **300 requests per minute**, counted **per agency** and shared across every API key the agency holds.
* **60 embed tokens per minute**, counted per API key.

Calls the embedded builder makes with a token are counted separately, per end user, so they don't eat into the 300.

Over any of them you get `429` with a `Retry-After` header.

## Related

* [Quickstart C — build forms via the API](/intake-forms/c-build-forms-api.md)
* [Embed the form builder](/intake-forms/builder-sessions.md)
* [Submissions](/intake-forms/submissions.md)
* [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/forms.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.
