> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.kota.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.kota.io/_mcp/server.

# Adaptive Requirements Guide

> Resolve action-required intents by retrieving dynamic requirement schemas, rendering them with @kotaio/adaptive-form, and submitting answers

Adaptive Requirements is Kota's mechanism for collecting extra data when an intent cannot continue from the data it already has. When a workflow enters `action_required`, Kota gives you an adaptive requirement identifier (`ar_...`), or lets you list the open requirements for the parent intent. Your integration retrieves the dynamic form schema, presents it to the right user, and submits answers back to Kota.

> **Info**
>
> Before starting, ensure you've completed:
>
> * [Authentication](/core-components/authentication): API key setup, environments, and idempotency
> * [API Integration Overview](/api): the intent-based pattern used across API-only flows
> * [Webhooks and events](/core-components/webhooks-and-events): receiving asynchronous status updates

---

## Overview

Some mutating Kota API calls cannot complete in a single request. Kota may need extra information from an employer, employee, dependant, or beneficiary before it can continue. When that happens, the API does not treat the workflow as failed. Instead, the parent intent moves to `action_required` and exposes one or more adaptive requirements.

### Building blocks

| Building block            | What it is                                                                               |
| ------------------------- | ---------------------------------------------------------------------------------------- |
| Adaptive Requirements API | HTTP endpoints to retrieve a requirement schema and submit answers.                      |
| `@kotaio/adaptive-form`   | A headless React package that renders adaptive requirement schemas as interactive forms. |

### What is a requirement?

A requirement is a single outstanding data request tied to a parent workflow, such as a group quote intent, group policy intent, or enrolment intent. It has:

* An opaque ID in the form `ar_` followed by 32 lowercase hexadecimal characters, for example `ar_550e8400e29b41d4a716446655440000`.
* A schema that defines what Kota needs to collect.
* A fulfillment lifecycle that ends once the requirement is satisfied.

The `ar_` requirement ID is the handle you need to carry forward. Persist it against the workflow that triggered `action_required` so the user can resume if they leave the flow.

### Lifecycle

```mermaid
flowchart TD
  call[Mutating Kota API call or intent update]
  action[Kota returns action_required]
  get["GET /requirements/{requirement_id}"]
  render[Render schema with adaptive-form]
  fill[User fills in the form]
  post["POST /requirements/{requirement_id}"]
  fulfilled[Requirement fulfilled]
  parent[Resume or re-check parent intent]

  call --> action
  action --> get
  get --> render
  render --> fill
  fill --> post
  post --> fulfilled
  fulfilled --> parent
```

> **Note**
>
> Fulfilling a requirement does not always mean the parent intent is complete. After the requirement is fulfilled, continue following the parent intent with webhooks or a retrieve call.

---

## Keep requirement calls server-side

The Adaptive Requirements API uses your Kota API secret key as a Bearer token. Your secret key must never reach the browser.

Render the form in the browser, but route `GET /requirements/{requirement_id}` and `POST /requirements/{requirement_id}` through your own backend:

```text
Browser (your UI + adaptive-form)
  |
  | fetch /your-backend/requirements/:id
  v
Your backend  -- Authorization: Bearer <secret key> -->  Kota API
```

| Environment | Base URL                   |
| ----------- | -------------------------- |
| Test        | `https://test.api.kota.io` |
| Production  | `https://api.kota.io`      |

> **Warning**
>
> Pair test keys with `https://test.api.kota.io` and live keys with `https://api.kota.io`. A requirement created in one environment will not exist in the other.

---

## Quickstart

### Step 1: Detect `action_required`

When a parent workflow needs extra data, persist the requirement ID and route the user to your requirement form.

Some flows expose the requirement ID directly on the action payload:

```json
{
  "state": "action_required",
  "requirement_id": "ar_550e8400e29b41d4a716446655440000"
}
```

Other intent flows let you list open requirements for the parent intent:

| Parent flow         | List endpoint                                                                                                                                  |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Group quote intent  | [`GET /group_quote_intents/{group_quote_intent_id}/requirements`](/api-reference/group-quote-intents/list-group-quote-intent-requirements)     |
| Group policy intent | [`GET /group_policy_intents/{group_policy_intent_id}/requirements`](/api-reference/group-policy-intents/list-group-policy-intent-requirements) |
| Enrolment intent    | [`GET /enrolment_intents/{enrolment_intent_id}/requirements`](/api-reference/enrolment-intents/list-enrolment-intent-requirements)             |

Each listed requirement includes an `id` (`ar_...`), `object_type`, `object_id`, `requirement_type`, and `is_fulfilled`. Use those fields to decide where the requirement belongs in your UI.

### Step 2: Retrieve the schema

Call Kota from your backend:

**`cURL`**

```bash cURL
curl "https://test.api.kota.io/requirements/ar_550e8400e29b41d4a716446655440000" \
  -H "Authorization: Bearer YOUR_API_SECRET_KEY"
```

Example response:

```json
{
  "is_fulfilled": false,
  "schema": {
    "id": "enrolment_employee",
    "version": 3,
    "object_type": "employee",
    "benefit_type": "health",
    "context": "enrolment_intent",
    "fields": [
      {
        "id": "first_name",
        "type": "text",
        "label": { "default": "First name" }
      },
      {
        "id": "date_of_birth",
        "type": "date",
        "label": { "default": "Date of birth" }
      }
    ]
  }
}
```

### Step 3: Render the form

Install the adaptive form package:

```bash
npm install @kotaio/adaptive-form
```

Render the schema in your frontend. Prefer controlled mode so you can submit a JSON `answers` object and preserve value types:

```tsx
import { useEffect, useState } from "react";
import { AdaptiveFormProvider, AdaptiveForm } from "@kotaio/adaptive-form/react";
import { TextInput, NumberInput, SelectInput, CheckboxInput, DateInput } from "./fields";

const components = {
  text: (props) => <TextInput {...props} />,
  number: (props) => <NumberInput {...props} />,
  select: (props) => <SelectInput {...props} />,
  checkbox: (props) => <CheckboxInput {...props} />,
  date: (props) => <DateInput {...props} />,
};

export function RequirementForm({ requirementId }: { requirementId: string }) {
  const [schema, setSchema] = useState<unknown>(null);
  const [formData, setFormData] = useState<Record<string, unknown>>({});

  useEffect(() => {
    fetch(`/api/requirements/${requirementId}`)
      .then((res) => res.json())
      .then((data) => {
        if (data.is_fulfilled) {
          // Already done - continue the parent workflow.
          return;
        }
        setSchema(data.schema);
      });
  }, [requirementId]);

  if (!schema) return <p>Loading...</p>;

  return (
    <AdaptiveFormProvider requirements={schema}>
      <AdaptiveForm
        value={formData}
        onChange={setFormData}
        components={components}
      />
      <button
        type="button"
        onClick={async () => {
          await fetch(`/api/requirements/${requirementId}`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ answers: formData }),
          });
        }}
      >
        Submit
      </button>
    </AdaptiveFormProvider>
  );
}
```

### Step 4: Submit answers

Submit answers from your backend:

**`cURL`**

```bash cURL
curl -X POST "https://test.api.kota.io/requirements/ar_550e8400e29b41d4a716446655440000" \
  -H "Authorization: Bearer YOUR_API_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9d3d2a34-4a56-4a1e-9f8e-7b6a9d2c4f10" \
  -d '{
    "answers": {
      "first_name": "Ada",
      "date_of_birth": "1990-12-10"
    }
  }'
```

Successful response when the schema is fully satisfied:

```json
{
  "status": "fulfilled"
}
```

If answers are stored but more fields are still required, the response stays `in_progress` and lists the outstanding field IDs:

```json
{
  "status": "in_progress",
  "outstanding_fields": ["date_of_birth"]
}
```

### Step 5: Resume the parent flow

Once the requirement is fulfilled, the parent intent can proceed. Depending on the flow, Kota may continue automatically or your integration may need to re-check the parent intent. Use the relevant webhook or retrieve endpoint to drive the next UI state.

---

## Retrieve a requirement

**Endpoint:** [`GET /requirements/{requirement_id}`](/api-reference/requirements/get-requirement)

```js
app.get("/api/requirements/:id", async (req, res) => {
  const response = await fetch(
    `https://test.api.kota.io/requirements/${req.params.id}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.KOTA_SECRET_KEY}`,
      },
    }
  );

  res.status(response.status).json(await response.json());
});
```

The response includes:

* `is_fulfilled`: whether the requirement is already complete
* `fulfilled_at`: present only when fulfilled
* `schema`: the fields Kota needs you to collect (present for pending requirements, and may also be returned when fulfilled)

Pass `schema` into `AdaptiveFormProvider`.

```json
{
  "is_fulfilled": false,
  "schema": {
    "id": "enrolment_employee",
    "version": 3,
    "object_type": "employee",
    "benefit_type": "health",
    "context": "enrolment_intent",
    "fields": [],
    "datasets": [],
    "flow": {}
  }
}
```

Treat the schema as fresh on every fetch. It is versioned and Kota can publish changes as provider requirements evolve.

> **Note**
>
> `GET` is safe to call repeatedly while the requirement is open. If `is_fulfilled` is `true`, treat that response as already done and continue the parent workflow.

---

## Render the form

`@kotaio/adaptive-form` is headless: it owns the schema logic, and you own the UI. It handles visibility rules, validation, read-only and computed values, multi-step requirements, and the final answer set.

The package has peer dependencies on `react` and `react-dom` version `>=18.3.1`.

### Wire it up

```tsx
import { AdaptiveFormProvider, AdaptiveForm } from "@kotaio/adaptive-form/react";

<AdaptiveFormProvider requirements={schema}>
  <AdaptiveForm
    value={formData}
    onChange={setFormData}
    components={{
      text: (props) => <TextInput {...props} />,
      number: (props) => <NumberInput {...props} />,
      select: (props) => <SelectInput {...props} />,
      checkbox: (props) => <CheckboxInput {...props} />,
      date: (props) => <DateInput {...props} />,
    }}
  />
</AdaptiveFormProvider>;
```

`AdaptiveForm` must render inside `AdaptiveFormProvider`.

### Build field components

```tsx
import type { FieldInputProps } from "@kotaio/adaptive-form/react";

function TextInput({
  field,
  value,
  onChange,
  onBlur,
  errors,
  isRequired,
  isVisible,
  isReadOnly,
  label,
}: FieldInputProps) {
  if (!isVisible) return null;

  return (
    <div>
      <label>
        {label}
        {isRequired && <span>*</span>}
      </label>
      <input
        name={field.id}
        value={(value as string) ?? ""}
        onChange={(event) => onChange(event.target.value)}
        onBlur={onBlur}
        placeholder={field.placeholder}
        readOnly={isReadOnly}
      />
      {errors.map((error, index) => (
        <p key={index} className="error">
          {error}
        </p>
      ))}
    </div>
  );
}
```

Common `FieldInputProps`:

| Prop           | Description                                       |
| -------------- | ------------------------------------------------- |
| `field`        | The field definition from the schema.             |
| `value`        | Current value.                                    |
| `onChange`     | Call this with the next value.                    |
| `onBlur`       | Call this on blur for touched-state tracking.     |
| `errors`       | Validation messages to display.                   |
| `isRequired`   | Whether the field is currently required.          |
| `isVisible`    | Whether the field should render.                  |
| `isReadOnly`   | Whether the field is read-only.                   |
| `isValidating` | Whether validation is in progress for this field. |
| `options`      | Resolved options for select or radio fields.      |
| `label`        | Resolved label after localization.                |

### Field types

Map every field type your schemas use to a component. Common types include `text`, `number`, `email`, `date`, `textarea`, `select`, `radio`, `checkbox`, `phone`, and `file`.

Two field types are special:

| Type       | How to handle                                                |
| ---------- | ------------------------------------------------------------ |
| `computed` | Read-only value filled by the form. Display only if visible. |
| `hidden`   | Do not render.                                               |

For full control over one field type, use `renderField`. It receives the full field state. Return `null` to fall back to the `components` map.

### Controlled and uncontrolled usage

* **Controlled (recommended for API submission):** pass `value` and `onChange`, then `POST` `{ answers: formData }` as JSON so booleans, numbers, and arrays keep their types.
* **Uncontrolled:** omit `value`/`onChange` (optionally pass `defaultValue`) and read values through native form submission. Convert carefully if you assemble the JSON payload yourself, because `FormData` stringifies values.

> **Warning**
>
> In controlled mode, define the `components` map outside the component or wrap it in `useMemo`. Inline objects can remount fields and cause users to lose focus while typing.

### Multi-step forms

Some requirements have multiple steps. `AdaptiveForm` renders one step at a time by default, includes Previous and Next navigation, and disables Next until the current step is valid.

Use:

* `renderStepNavigation` for custom navigation.
* `showAllSteps` to render every step on one page.
* `useFormInfo()` for step state, such as a progress indicator.

### Form library adapters

```tsx
import { useReactHookFormAdapter } from "@kotaio/adaptive-form/react/adapters/react-hook-form";
// or
import { useFormikAdapter } from "@kotaio/adaptive-form/react/adapters/formik";

const { value, onChange } = useReactHookFormAdapter({ form });

<AdaptiveForm value={value} onChange={onChange} components={myComponents} />;
```

Both adapters accept optional `serialize` and `deserialize` functions. By default, Date values serialize to `YYYY-MM-DD`.

---

## Submit answers

**Endpoint:** [`POST /requirements/{requirement_id}`](/api-reference/requirements/submit-answers)

The payload is a single `answers` object keyed by field ID.

```json
{
  "answers": {
    "first_name": "Ada",
    "date_of_birth": "1990-12-10",
    "has_dependants": true
  }
}
```

Values keep their JSON types. Send booleans as `true` or `false`, numbers as numbers, and dates as `YYYY-MM-DD`.

> **Info**
>
> Answers are merged with any previously submitted values and re-evaluated against the requirement schema. The requirement becomes `fulfilled` once all required fields are satisfied. Otherwise it stays `in_progress` and the response lists `outstanding_fields`. A required field that has not been submitted yet is not an error.

In practice, submit the complete answer set produced by `@kotaio/adaptive-form`. That keeps client and server state aligned and avoids leaving fields outstanding unintentionally.

Use an `Idempotency-Key` for safe retries on mutating requests:

```bash
-H "Idempotency-Key: YOUR_UNIQUE_KEY"
```

Generate one key per logical submission and reuse it only for retries of that exact same submission. See [Authentication and idempotency](/core-components/authentication#idempotent-requests).

| Case                                       | Response                                       |
| ------------------------------------------ | ---------------------------------------------- |
| Same key, same body                        | Original cached response.                      |
| Same key, different body or endpoint       | `409` idempotency conflict.                    |
| Same key while first request is processing | `409` in-progress conflict with `Retry-After`. |

### Validation

`@kotaio/adaptive-form` validates input as the user types. Treat client-side validation as the primary gate and disable submit until the form is valid.

Kota also validates server-side. If the API returns `400 invalid_request` with an `errors` array, map each error back to the field with the same `field_id`:

```json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Invalid request",
  "status": 400,
  "error_code": "invalid_request",
  "trace_id": "3643f86ed46106566fc9ffac4f811800",
  "detail": "Answer validation failed",
  "errors": [
    {
      "field_id": "date_of_birth",
      "error_type": "rule",
      "message": "Date of birth cannot be in the future"
    }
  ]
}
```

---

## Errors

Adaptive Requirements errors use `application/problem+json` and follow the same general shape as the rest of the Kota API.

```json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Resource not found",
  "status": 404,
  "error_code": "not_found",
  "trace_id": "3643f86ed46106566fc9ffac4f811800",
  "detail": "Requirement ar_550e8400e29b41d4a716446655440000 not found"
}
```

Branch on `error_code`, not only HTTP status. Some statuses, especially `400` and `409`, cover multiple cases.

| Status | `error_code`                      | Endpoints     | Meaning                                                             | What to do                                                       |
| ------ | --------------------------------- | ------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `400`  | `invalid_request`                 | `POST`        | Answers failed validation and `errors[]` is present.                | Surface each message against its field by `field_id`.            |
| `400`  | `invalid_request`                 | `GET`, `POST` | Request shape is malformed and `issues[]` is present.               | Fix the request shape.                                           |
| `400`  | `invalid_request`                 | `POST`        | Unknown field IDs were submitted and `unknown_fields[]` is present. | Align with the current schema.                                   |
| `401`  | `unauthorized`                    | `GET`, `POST` | Secret key is missing, invalid, or expired.                         | Send the correct Bearer token for the environment.               |
| `404`  | `not_found`                       | `GET`, `POST` | No requirement exists for that ID.                                  | Check the captured ID and environment.                           |
| `409`  | `requirement_already_fulfilled`   | `POST`        | The requirement was already submitted.                              | Treat as done and continue the workflow.                         |
| `409`  | `idempotency_error`               | `POST`        | Key reused with a different body or endpoint.                       | Use a fresh key for a new submission.                            |
| `409`  | `idempotency_request_in_progress` | `POST`        | A request with this key is still in flight.                         | Retry with the same key after `Retry-After`.                     |
| `500`  | `internal_error`                  | `GET`, `POST` | Schema could not be resolved or an internal error occurred.         | Retry with backoff. If persistent, contact Kota with `trace_id`. |

Retry only when it is safe:

* Retry `409 idempotency_request_in_progress` with the same key after `Retry-After`.
* Retry `500` responses with backoff.
* Do not retry `400`, `401`, `404`, `409 requirement_already_fulfilled`, or `409 idempotency_error` without changing the workflow or request.

---

## Best practices and gotchas

### Architecture

* **Keep the secret key on your server.** Proxy both requirement calls through your backend.
* **Persist the requirement ID immediately.** The `ar_` ID is the handle you need to resume.
* **Use the right environment and key.** Test requirements return `404` in production and production requirements return `404` in test.

### Submission

* **Submit the form's answer set as JSON.** Preserve booleans, numbers, and dates as typed JSON values.
* **Handle already fulfilled as done.** On `GET`, check `is_fulfilled`. On `POST`, treat `409 requirement_already_fulfilled` as success for the workflow.
* **Make retries idempotent.** Send an `Idempotency-Key` and reuse it only for retries of the same submission.
* **Expect incremental progress.** Partial submissions can return `in_progress` with `outstanding_fields` until the schema is complete.

### Schema handling

* **Always fetch fresh.** The schema is versioned and may change as provider requirements evolve.
* **Let the form do the logic.** Pass the schema to `@kotaio/adaptive-form` instead of rebuilding schema rules yourself.
* **Validate client-side before submitting.** Disable submit until the form reports no errors.
* **Clear hidden values.** Use `clearHiddenValues` so values entered before a field is hidden are not submitted.

### `@kotaio/adaptive-form`

* **Memoize components in controlled mode.** Inline component maps can remount inputs.
* **Return `null` when `isVisible` is false.** Otherwise you may render fields the schema intends to hide.
* **Preserve boolean option values.** Stringify only for display, and pass the raw value to `onChange`.
* **Keep the package up to date.** Older versions may not understand newer schema constructs.

---

## Next steps

* [Employee Enrolment](/api/employee-flow/employee-enrolment): handle enrolment intent statuses and action-required states
* [Employer Health Insurance Setup](/api/employer-health-insurance): handle requirements during quote and setup flows
* [API Reference: Requirements](/api-reference/requirements/get-requirement): retrieve requirement schemas and submit answers
* [Webhooks and events](/core-components/webhooks-and-events): react to parent intent status changes
* [API Errors](/api-reference/errors): understand error response format