> 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

**What you'll build:** a form created, structured, and published entirely from your backend — no dashboard clicking required. By the end you'll have a live, fillable form with a conditional-logic rule.

**Time:** \~20 minutes.

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

* [ ] An InsurGrid account with access to the **API section**.
* [ ] Your **management token**, with owner/admin permissions (from [API credentials](/get-started/03-api-credentials.md)).
* [ ] Node 18+ *or* Python 3.9+ installed.

> This quickstart is **API-only** — it doesn't need a receiving webhook endpoint or a public tunnel. If you also want to collect submissions, do that separately in [Quickstart B](/intake-forms/b-embed-form.md) once your form is published.

***

## Step 1 — Create a draft form · *server*

`POST /v2/forms` creates a `draft` form and returns it with a new `id` and `public_id`. Every write call needs your **management token** and an **`Idempotency-Key`** header.

> **New term — management token:** a server-to-server bearer credential InsurGrid issues to your account. It authorizes every form read/write and submission read — never expose it in browser code. Send it as `Authorization: Bearer <token>`.

> **New term — Idempotency-Key:** a UUIDv4 you generate and send with a write request. If the request is retried with the same key (a network blip, a timeout on your side), InsurGrid returns the original result instead of performing the operation again — so a retry can never create a duplicate form. Use a fresh key per distinct operation.

**Node:**

```js
// create-form.js — run with: node create-form.js
const { randomUUID } = require("crypto");

const res = await fetch("https://api.insurgrid.com/v2/forms", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.INSURGRID_API_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    name: "Apartment Intake — Acme Agency",
    owner: { type: "agency", agency_id: 1201 },
    insurance_line: "commercial",
  }),
});

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

**Python:**

```python
# create_form.py — run with: python create_form.py
import os, uuid, requests

res = requests.post(
    "https://api.insurgrid.com/v2/forms",
    headers={
        "Authorization": f"Bearer {os.environ['INSURGRID_API_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Apartment Intake — Acme Agency",
        "owner": {"type": "agency", "agency_id": 1201},
        "insurance_line": "commercial",
    },
)

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

Request and response:

```
// 201 Created
{
  "id": 4412,
  "public_id": "f_9c2a7b1e4d",
  "name": "Apartment Intake — Acme Agency",
  "status": "draft",
  "insurance_line": "commercial",
  "owner": { "type": "agency", "agency_id": 1201 },
  "steps": [],
  "created_at": "2026-09-04T15:10:00Z",
  "updated_at": "2026-09-04T15:10:00Z"
}
```

> Only owner/admin roles can create or manage forms. A write call outside your permissions returns `403`; a form outside your visibility returns `404`. See Ownership model.

Want to start from a pre-built form instead of an empty one? Send `template_id` (`auto`, `home`, `intake`, `habitational`) instead of building `steps` from scratch, then `PATCH` to customize. Full request/response shapes: Forms API reference.

## Step 2 — Add steps, questions, and a conditional-logic rule · *server*

A form is an ordered list of **steps**, each holding ordered **questions**. `PATCH /v2/forms/{id}` with a `steps` array replaces the form's structure — send the whole thing you want, not a diff.

> **New term — conditional logic:** a rule attached to a question that shows, hides, requires, or skips based on an earlier answer — so a question like "Legal entity name" only appears when the consumer picked "Commercial" a step earlier.

This example adds one step with two questions: a `single_choice` question, and a second question that's hidden until the first is answered `"Commercial"`.

**Node:**

```js
const res = await fetch(`https://api.insurgrid.com/v2/forms/${form.id}`, {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${process.env.INSURGRID_API_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    steps: [
      {
        type: "custom", name: "Applicant", segment: "before", position: 1,
        questions: [
          {
            id: "q-policy-kind", type: "single_choice", label: "Policy kind",
            required: true, options: ["Personal", "Commercial"],
          },
          {
            type: "short_text", label: "Legal entity name", initially_hidden: true,
            rules: [
              {
                condition_mode: "all",
                conditions: [
                  { source_question_id: "q-policy-kind", operator: "equals", value: "Commercial" },
                ],
                action: "show",
              },
            ],
          },
        ],
      },
    ],
  }),
});
```

**Python:**

```python
res = requests.patch(
    f"https://api.insurgrid.com/v2/forms/{form['id']}",
    headers={
        "Authorization": f"Bearer {os.environ['INSURGRID_API_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "steps": [
            {
                "type": "custom", "name": "Applicant", "segment": "before", "position": 1,
                "questions": [
                    {
                        "id": "q-policy-kind", "type": "single_choice", "label": "Policy kind",
                        "required": True, "options": ["Personal", "Commercial"],
                    },
                    {
                        "type": "short_text", "label": "Legal entity name", "initially_hidden": True,
                        "rules": [
                            {
                                "condition_mode": "all",
                                "conditions": [
                                    {"source_question_id": "q-policy-kind",
                                     "operator": "equals", "value": "Commercial"},
                                ],
                                "action": "show",
                            }
                        ],
                    },
                ],
            }
        ],
    },
)
```

| Rule field       | Meaning                                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `condition_mode` | `all` (AND) or `any` (OR) across the rule's conditions.                                                                  |
| `conditions[]`   | Each: `{ source_question_id, operator, value }`.                                                                         |
| `operator`       | `equals`, `notEquals`, `contains`, `notContains`, `isEmpty`, `isNotEmpty`, `greaterThan`, `lessThan`, `before`, `after`. |
| `action`         | `show`, `hide`, `require`, `skip_to_step`, `end_form`, `continue`.                                                       |

Available question `type`s: `short_text`, `long_text`, `email`, `number`, `date`, `dropdown`, `single_choice`, `multi_choice`, `file_upload`, `address`, `data_table`. This is also the exact shape `GET /v2/forms/{id}` returns — the same model on the way in and out. Full reference: Form structure & conditional logic.

## Step 3 — Publish the form · *server*

`POST /v2/forms/{id}/publish` transitions `draft` → `active`. Once active, the form is live at its public URL and accepts submissions.

```bash
curl -X POST https://api.insurgrid.com/v2/forms/4412/publish \
  -H "Authorization: Bearer $INSURGRID_API_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
```

```
// 200 OK
{ "id": 4412, "public_id": "f_9c2a7b1e4d", "status": "active", … }
```

> ⚠️ **Publishing overwrites the current live form.** There's no version history yet — editing and re-publishing replaces the form in place. If you need to preserve an old structure, duplicate it first with `POST /v2/forms/{id}/duplicate`.

## Step 4 — Embed it

Your form is now live at its public URL. Drop it into your app and start receiving submissions by webhook — that's [Quickstart B](/intake-forms/b-embed-form.md), starting from Step 2 (you already have the `public_id` from Step 1 here).

***

## Why the `Idempotency-Key`?

Every write in this quickstart — create, update, publish — sent a fresh `Idempotency-Key`. Networks drop responses, not always requests: if your create call times out but actually succeeded on InsurGrid's side, retrying blind would mint a second form. Retrying with the *same* key instead returns the original result. Keys are honored for 24 hours; use a new one per distinct operation, and reuse a key only when retrying that exact same operation. Deeper dive: Idempotency keys.

## Next steps

* [Embed a form & receive submissions →](/intake-forms/b-embed-form.md)
* Forms API reference (full read/write surface) →
* Form structure & conditional logic →
* Embedded form builder (build visually instead) →


---

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