Adaptive Requirements Guide

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.

Before starting, ensure you’ve completed:


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 blockWhat it is
Adaptive Requirements APIHTTP endpoints to retrieve a requirement schema and submit answers.
@kotaio/adaptive-formA 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

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:

Browser (your UI + adaptive-form)
|
| fetch /your-backend/requirements/:id
v
Your backend -- Authorization: Bearer <secret key> --> Kota API
EnvironmentBase URL
Testhttps://test.api.kota.io
Productionhttps://api.kota.io

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:

1{
2 "state": "action_required",
3 "requirement_id": "ar_550e8400e29b41d4a716446655440000"
4}

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

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
$curl "https://test.api.kota.io/requirements/ar_550e8400e29b41d4a716446655440000" \
> -H "Authorization: Bearer YOUR_API_SECRET_KEY"

Example response:

1{
2 "is_fulfilled": false,
3 "schema": {
4 "id": "enrolment_employee",
5 "version": 3,
6 "object_type": "employee",
7 "benefit_type": "health",
8 "context": "enrolment_intent",
9 "fields": [
10 {
11 "id": "first_name",
12 "type": "text",
13 "label": { "default": "First name" }
14 },
15 {
16 "id": "date_of_birth",
17 "type": "date",
18 "label": { "default": "Date of birth" }
19 }
20 ]
21 }
22}

Step 3: Render the form

Install the adaptive form package:

$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:

1import { useEffect, useState } from "react";
2import { AdaptiveFormProvider, AdaptiveForm } from "@kotaio/adaptive-form/react";
3import { TextInput, NumberInput, SelectInput, CheckboxInput, DateInput } from "./fields";
4
5const components = {
6 text: (props) => <TextInput {...props} />,
7 number: (props) => <NumberInput {...props} />,
8 select: (props) => <SelectInput {...props} />,
9 checkbox: (props) => <CheckboxInput {...props} />,
10 date: (props) => <DateInput {...props} />,
11};
12
13export function RequirementForm({ requirementId }: { requirementId: string }) {
14 const [schema, setSchema] = useState<unknown>(null);
15 const [formData, setFormData] = useState<Record<string, unknown>>({});
16
17 useEffect(() => {
18 fetch(`/api/requirements/${requirementId}`)
19 .then((res) => res.json())
20 .then((data) => {
21 if (data.is_fulfilled) {
22 // Already done - continue the parent workflow.
23 return;
24 }
25 setSchema(data.schema);
26 });
27 }, [requirementId]);
28
29 if (!schema) return <p>Loading...</p>;
30
31 return (
32 <AdaptiveFormProvider requirements={schema}>
33 <AdaptiveForm
34 value={formData}
35 onChange={setFormData}
36 components={components}
37 />
38 <button
39 type="button"
40 onClick={async () => {
41 await fetch(`/api/requirements/${requirementId}`, {
42 method: "POST",
43 headers: { "Content-Type": "application/json" },
44 body: JSON.stringify({ answers: formData }),
45 });
46 }}
47 >
48 Submit
49 </button>
50 </AdaptiveFormProvider>
51 );
52}

Step 4: Submit answers

Submit answers from your backend:

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:

1{
2 "status": "fulfilled"
3}

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

1{
2 "status": "in_progress",
3 "outstanding_fields": ["date_of_birth"]
4}

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}

1app.get("/api/requirements/:id", async (req, res) => {
2 const response = await fetch(
3 `https://test.api.kota.io/requirements/${req.params.id}`,
4 {
5 headers: {
6 Authorization: `Bearer ${process.env.KOTA_SECRET_KEY}`,
7 },
8 }
9 );
10
11 res.status(response.status).json(await response.json());
12});

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.

1{
2 "is_fulfilled": false,
3 "schema": {
4 "id": "enrolment_employee",
5 "version": 3,
6 "object_type": "employee",
7 "benefit_type": "health",
8 "context": "enrolment_intent",
9 "fields": [],
10 "datasets": [],
11 "flow": {}
12 }
13}

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

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

1import { AdaptiveFormProvider, AdaptiveForm } from "@kotaio/adaptive-form/react";
2
3<AdaptiveFormProvider requirements={schema}>
4 <AdaptiveForm
5 value={formData}
6 onChange={setFormData}
7 components={{
8 text: (props) => <TextInput {...props} />,
9 number: (props) => <NumberInput {...props} />,
10 select: (props) => <SelectInput {...props} />,
11 checkbox: (props) => <CheckboxInput {...props} />,
12 date: (props) => <DateInput {...props} />,
13 }}
14 />
15</AdaptiveFormProvider>;

AdaptiveForm must render inside AdaptiveFormProvider.

Build field components

1import type { FieldInputProps } from "@kotaio/adaptive-form/react";
2
3function TextInput({
4 field,
5 value,
6 onChange,
7 onBlur,
8 errors,
9 isRequired,
10 isVisible,
11 isReadOnly,
12 label,
13}: FieldInputProps) {
14 if (!isVisible) return null;
15
16 return (
17 <div>
18 <label>
19 {label}
20 {isRequired && <span>*</span>}
21 </label>
22 <input
23 name={field.id}
24 value={(value as string) ?? ""}
25 onChange={(event) => onChange(event.target.value)}
26 onBlur={onBlur}
27 placeholder={field.placeholder}
28 readOnly={isReadOnly}
29 />
30 {errors.map((error, index) => (
31 <p key={index} className="error">
32 {error}
33 </p>
34 ))}
35 </div>
36 );
37}

Common FieldInputProps:

PropDescription
fieldThe field definition from the schema.
valueCurrent value.
onChangeCall this with the next value.
onBlurCall this on blur for touched-state tracking.
errorsValidation messages to display.
isRequiredWhether the field is currently required.
isVisibleWhether the field should render.
isReadOnlyWhether the field is read-only.
isValidatingWhether validation is in progress for this field.
optionsResolved options for select or radio fields.
labelResolved 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:

TypeHow to handle
computedRead-only value filled by the form. Display only if visible.
hiddenDo 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.

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

1import { useReactHookFormAdapter } from "@kotaio/adaptive-form/react/adapters/react-hook-form";
2// or
3import { useFormikAdapter } from "@kotaio/adaptive-form/react/adapters/formik";
4
5const { value, onChange } = useReactHookFormAdapter({ form });
6
7<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}

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

1{
2 "answers": {
3 "first_name": "Ada",
4 "date_of_birth": "1990-12-10",
5 "has_dependants": true
6 }
7}

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

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:

$-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.

CaseResponse
Same key, same bodyOriginal cached response.
Same key, different body or endpoint409 idempotency conflict.
Same key while first request is processing409 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:

1{
2 "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
3 "title": "Invalid request",
4 "status": 400,
5 "error_code": "invalid_request",
6 "trace_id": "3643f86ed46106566fc9ffac4f811800",
7 "detail": "Answer validation failed",
8 "errors": [
9 {
10 "field_id": "date_of_birth",
11 "error_type": "rule",
12 "message": "Date of birth cannot be in the future"
13 }
14 ]
15}

Errors

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

1{
2 "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
3 "title": "Resource not found",
4 "status": 404,
5 "error_code": "not_found",
6 "trace_id": "3643f86ed46106566fc9ffac4f811800",
7 "detail": "Requirement ar_550e8400e29b41d4a716446655440000 not found"
8}

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

Statuserror_codeEndpointsMeaningWhat to do
400invalid_requestPOSTAnswers failed validation and errors[] is present.Surface each message against its field by field_id.
400invalid_requestGET, POSTRequest shape is malformed and issues[] is present.Fix the request shape.
400invalid_requestPOSTUnknown field IDs were submitted and unknown_fields[] is present.Align with the current schema.
401unauthorizedGET, POSTSecret key is missing, invalid, or expired.Send the correct Bearer token for the environment.
404not_foundGET, POSTNo requirement exists for that ID.Check the captured ID and environment.
409requirement_already_fulfilledPOSTThe requirement was already submitted.Treat as done and continue the workflow.
409idempotency_errorPOSTKey reused with a different body or endpoint.Use a fresh key for a new submission.
409idempotency_request_in_progressPOSTA request with this key is still in flight.Retry with the same key after Retry-After.
500internal_errorGET, POSTSchema 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