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

# Quickstart C — Build forms via the API

Create, structure and publish a form entirely from your backend.

If your users build their forms inside your product, you have two options: embed InsurGrid's builder, or drive the API yourself and put your own interface on top. This walkthrough is the second.

By the end you'll have created a form, given it questions, and published it — without opening a dashboard. About 20 minutes.

**What you'll need**

* An InsurGrid account with Forms access enabled.
* An API key belonging to an **agency admin** ([where to get one](/get-started/03-api-credentials.md)), kept on your server.

> No webhook endpoint or tunnel needed here — this is API-only. To collect the submissions as well, do [Quickstart B](/intake-forms/b-embed-form.md) once your form is live.

| Environment | Base URL                                             |
| ----------- | ---------------------------------------------------- |
| Staging     | `https://phoenix-staging.insurgrid.com/api/v1/forms` |
| Production  | `https://phoenix.insurgrid.com/api/v1/forms`         |

Only the host differs — see [Environments](/guides/environments.md).

***

## 1. Create a draft

Every form belongs to one of your users. `externalUserId` says which, it's required, and it decides who can see the form from then on.

```js
const res = await fetch("https://phoenix-staging.insurgrid.com/api/v1/forms", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.INSURGRID_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Apartment Intake — Acme Agency",
    externalUserId: "user-8841",
    insurance_line: "commercial",
  }),
});

const form = await res.json();
console.log(form.id, form.public_id, form.status);   // "draft"
```

<details>

<summary>Python</summary>

```python
import os, requests

res = requests.post(
    "https://phoenix-staging.insurgrid.com/api/v1/forms",
    headers={"Authorization": f"Bearer {os.environ['INSURGRID_API_KEY']}"},
    json={
        "name": "Apartment Intake — Acme Agency",
        "externalUserId": "user-8841",
        "insurance_line": "commercial",
    },
)

form = res.json()
print(form["id"], form["public_id"], form["status"])   # "draft"
```

</details>

Forms are always created as drafts — you can't create one already live.

Because we left `steps` out, the form comes with a working default: a welcome step, a **contact info step** already collecting name, email and phone, and a confirmation step. That contact step matters more than it looks — see step 2.

| Field            | Required | Notes                                                                             |
| ---------------- | -------- | --------------------------------------------------------------------------------- |
| `externalUserId` | Yes      | Whose form this is. Missing → `422 external_user_required`.                       |
| `name`           | Yes      | Up to 255 characters.                                                             |
| `slug`           | No       | e.g. `home-intake`. Generated from `name` if omitted, with a short random suffix. |
| `insurance_line` | No       | `personal` (default) or `commercial`.                                             |
| `steps`          | No       | The pages and questions. Omit for the default above.                              |
| `redirect_url`   | No       | Where to send the customer after they submit.                                     |
| `branding`       | No       | Logo and colours.                                                                 |

### Starting from something

Rather than an empty form:

```bash
# Ready-made starter forms
curl 'https://phoenix-staging.insurgrid.com/api/v1/forms/templates' \
  -H 'Authorization: Bearer YOUR_API_KEY'

# ACORD forms and their questions
curl 'https://phoenix-staging.insurgrid.com/api/v1/forms/acord-catalog?line=personal' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

> ⚠️ **`line` is required on `acord-catalog`.** Without it you get `422 validation_error`, and an unrecognised value is a `400 invalid_request`. Use `personal` or `commercial`.

## 2. Give it questions

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

```json
{
  "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+" }] }
      ]
    }
  ]
}
```

On a step, `type`, `name`, `segment` and `position` are all required — `segment` is the one people miss, since it has no default. On a question, `type`, `label` and `position` are required. The full field tables are on the [Forms API](/intake-forms/forms.md) page.

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

Send it with `PATCH`:

```bash
curl -X PATCH 'https://phoenix-staging.insurgrid.com/api/v1/forms/4412' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{ "steps": [ … ] }'
```

> ⚠️ **`steps` replaces, it doesn't merge.** Send the complete list — anything you leave out is deleted.

### Don't lose the contact block

That replacement includes the default **contact info** step you got in step 1 — and with it, the tags that populate the `contact` object on every submission webhook.

InsurGrid fills `contact.name`, `contact.email` and `contact.phone` from questions carrying a `field_mapping`, not from a question's type or label. Replace `steps` without tagging your own questions and every webhook from then on arrives with:

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

Nothing errors, and the answers are still in `answers[]` — only `contact` goes empty, which is easy to miss until something downstream quietly stops working.

So tag them:

```json
{ "type": "short_text", "label": "Full name", "position": 0, "field_mapping": "FullName" },
{ "type": "email",      "label": "Email",     "position": 1, "field_mapping": "Email" },
{ "type": "short_text", "label": "Phone",     "position": 2, "field_mapping": "Phone" }
```

Or keep a `contact_info` step in your own structure and let it do the job.

The shape you send is exactly what `GET /forms/{id}` returns, so the quickest way to learn it is to build one form the way you want it **in the embedded builder**, fetch it, and work from that. Forms built in the InsurGrid dashboard won't do — this API can't see them.

`externalUserId` can't be changed after creation — it isn't a field on the update call. On `PATCH`, unknown fields are rejected, so a misspelled key is a `422`.

## 3. Publish it

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

A **`PUT`**, not a `POST`. The form becomes `active` and its `shareUrl` — `null` until now — is filled in with the address people visit to fill it in.

Later, `PUT /{id}/archive` takes it out of use and `PUT /{id}/reactivate` brings it back.

> ⚠️ **Publishing replaces the live form in place.** Forms aren't versioned, so if you need to keep an older structure, copy it first with `POST /forms/{id}/duplicate`.

## 4. Put it in front of people

Your form is live at its `shareUrl`. Share it and start receiving submissions — that's [Quickstart B](/intake-forms/b-embed-form.md).

> Carrier-connected forms, which log into a carrier to pull declaration pages, can't be created here — the request is rejected with `422 carrier_flow_disabled`. Build those in the dashboard.

***

## Next steps

* [Share a form & receive submissions](/intake-forms/b-embed-form.md)
* [Forms API](/intake-forms/forms.md) — the full field, error and rate-limit reference
* [Embed the form builder](/intake-forms/builder-sessions.md) — let your users build visually instead
* [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/c-build-forms-api.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.
