# Agent intake
Let ChatGPT, Claude, or another AI agent complete an insurance, factoring, or mortgage intake over the applicant API, the TypeScript SDK, or the MCP server
Source: https://www.klarefi.com/docs/guides/agent-intake

## Overview

Hosted intake sessions are agent-operable. Any agent that holds a signed
session link can read the session state, fill the form, answer follow-up
questions, upload documents, and submit, using the same endpoints the hosted
UI uses.

> **Use the canonical or discovered URL**
>
> Customer-plane integrations should copy their API base URL from
> **Settings → Developers**, or use `https://www.klarefi.com`. For applicant-plane discovery,
> `/s/{sessionId}/agent.json` carries the correct API base URL for that session;
> prefer the URL returned by discovery.

Two kinds of agents fit this model:

- The applicant's own agent, completing the intake on the applicant's behalf.
- Your agent, completing the parts your systems already know before handing
  the session to the applicant.

An agent can only complete the intake. It cannot approve, reject, or otherwise
decide the case. Human review stays in Klarefi.

## Two trust planes

Agent intake separates who integrates from who completes.

| Plane           | Credential                                                                        | Surface                                                  | Used for                                                                                            |
| --------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Customer plane  | `sk_live_` / `sk_test_` API key                                                   | `/api/v1/*` REST, `@klarefi/node`                        | Your backend or agents: create sessions, prefill known values, read cases, get the decision package |
| Applicant plane | Per-session bearer token (the `token` query parameter of the signed session link) | Session endpoints under `/api/v1/sessions/{sessionId}/*` | Whoever completes the intake: the applicant's agent, or your agent before handoff                   |

The applicant plane needs no API key. The token in the signed link is the
credential, scoped to one session. Treat the link as scoped applicant access:
anyone who holds it can act on that session until it expires or is revoked.

## The agent loop

Applicant-plane agents run one loop: read state, act, wait, repeat until done.

<Mermaid
  title="Agent intake loop"
  chart={`sequenceDiagram
  participant Agent
  participant K as Klarefi

Agent->>K: GET /api/v1/sessions/{sessionId}
K-->>Agent: state_pointer + view
Agent->>K: POST .../draft or .../replies or .../uploads
K-->>Agent: accepted, session enters processing
Agent->>K: GET .../events?after_cursor=N
K-->>Agent: gap.prompted (new question ready)
Agent->>K: POST .../replies
Agent->>K: POST .../submit
K-->>Agent: submitted, phase moves toward done
`}
/>

1. **Read state.** `GET /api/v1/sessions/{sessionId}` returns the current
   phase, the active task, and submit readiness.
2. **Act.** Save draft form values, answer the active question, upload a
   document, or submit.
3. **Wait.** After a reply the session enters `processing` while facts are
   resolved. Poll the events endpoint or re-read state until the next question
   is ready.
4. Repeat until the phase is `done`.

## Read session state

Authenticate every applicant-plane request with the session token as a bearer
token:

```http
GET /api/v1/sessions/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer 1751879000.9f2c4e...
```

```json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "state_pointer": {
    "phase": "question",
    "active_task_id": "task_7f3a",
    "submit_eligible": false,
    "submit_blocking_reasons": ["unresolved_follow_up"],
    "events_cursor": 14
  },
  "view": {
    "session": { "...": "..." },
    "applicant_projection": { "...": "..." },
    "session_progress": { "...": "..." }
  }
}
```

`state_pointer` is the small part an agent should branch on. `view` is the full
hosted context: form blocks, draft values, open tasks with typed response
fields, uploads, and progress.

The phase drives the loop:

| Phase        | Meaning                                                         |
| ------------ | --------------------------------------------------------------- |
| `form`       | Structured form fields are open. Save drafts, then submit.      |
| `processing` | Klarefi is resolving facts. Wait and poll.                      |
| `question`   | A follow-up question is active. Answer it or upload a document. |
| `done`       | No applicant input remains. The case moves to review.           |
| `failed`     | The session cannot continue. Surface the state to a human.      |

## Act on the session

All action endpoints live under
`/api/v1/sessions/{sessionId}` and use the same bearer token.

### Record consent first

When your organization requires consent, nothing is accepted before it. The
manifest carries `consent.pending` and, while it is pending, `consent.notice`:
the sentence the applicant agrees to, with the lawful basis, retention and
privacy notice. Show the applicant that notice. Once they agree, record it:

```http
POST /api/v1/sessions/{sessionId}/consent
Authorization: Bearer <session token>
```

The consent event records that an agent gave it and which client it was.
Repeating the call changes nothing. An applicant who opens the hosted page
consents there instead, by continuing past the same notice.

Until consent is recorded, the manifest marks `save_draft`, the upload actions
and `submit` unavailable with a `consent required` reason, calling them returns
`409 consent_required`, and `submit_blocking_reasons` includes
`Privacy consent is required.` In the SDK the state's `next_action` is
`{ type: "consent", notice }` and `session.giveConsent()` records it. The MCP
tool is `give_intake_consent`.

### Save draft form values

```http
POST /api/v1/sessions/{sessionId}/draft
Authorization: Bearer <token>
Content-Type: application/json

{
  "client_mutation_id": "d3b1c9e0-5f4a-4b2e-9c1d-8a7f6e5d4c3b",
  "updated_fields": {
    "claimant_name": "Ada Lovelace",
    "claim_number": "CLM-123"
  }
}
```

Drafts do not trigger fact resolution. Use them to fill the form
incrementally, then submit.

### Answer the active question

```http
POST /api/v1/sessions/{sessionId}/replies
Authorization: Bearer <token>
Content-Type: application/json

{
  "content": "The vehicle was parked at the time of the incident.",
  "active_task_id": "task_7f3a",
  "idempotency_key": "reply:task_7f3a:14"
}
```

To answer several open tasks in one request, send
`{ "replies": [ ... ] }` with the same fields per entry.

### Upload a document

Uploads are three steps: initialize, transfer, complete.

```http
POST /api/v1/sessions/{sessionId}/uploads
Authorization: Bearer <token>
Content-Type: application/json

{
  "upload_id": "b6f0a1d2-3c4e-4f5a-8b9c-0d1e2f3a4b5c",
  "filename": "policy-card.pdf",
  "mime_type": "application/pdf",
  "size_bytes": 482113,
  "active_task_id": "task_9c2b"
}
```

The response contains `doc_id` and a short-lived `upload_url`. Send the file
bytes there with an HTTP `PUT`, then confirm:

```http
POST /api/v1/sessions/{sessionId}/uploads/{docId}/complete
Authorization: Bearer <token>
Content-Type: application/json

{
  "storage_id": "st_a1b2c3",
  "active_task_id": "task_9c2b"
}
```

Uploads are limited to 25MB. Each upload task lists its accepted MIME types;
the manifest carries the session-wide policy.

### Submit

```http
POST /api/v1/sessions/{sessionId}/submit
Authorization: Bearer <token>
Content-Type: application/json

{
  "submission_id": "e8c7d6b5-a4f3-4e2d-9c1b-0a9f8e7d6c5b"
}
```

If the session is not ready, the API responds `409` with the code
`submit_blocked` and the list of `blocking_reasons`. Resolve them (answer the
open question, provide the missing upload) and retry with the same
`submission_id`.

## Wait for the next step

After a reply or submit, the session enters `processing` while Klarefi
resolves facts. Two equivalent ways to wait:

- Poll `GET /api/v1/sessions/{sessionId}/events?after_cursor=N`. A
  `gap.prompted` event means a new question is ready.
- Re-read `GET /api/v1/sessions/{sessionId}` and check `state_pointer.phase`.

```http
GET /api/v1/sessions/{sessionId}/events?after_cursor=14
Authorization: Bearer <token>
```

```json
{
  "events": [
    {
      "event_id": "evt_5c1d",
      "event_type": "gap.prompted",
      "cursor": 15,
      "created_at": "2026-07-07T09:14:03Z",
      "payload": {
        "active_task_id": "task_7f3a",
        "task_ids": ["task_7f3a"]
      }
    }
  ],
  "next_cursor": 15,
  "has_more": false,
  "phase": "question"
}
```

Pass `next_cursor` as the next `after_cursor`. Poll no faster than every 3
seconds, and respect the 3-second `Retry-After` header when the API sends it.

## Discover capabilities

`GET /api/v1/sessions/{sessionId}/manifest` returns a machine-readable
descriptor of what the session supports right now:

```json
{
  "schema_version": "hosted_session_manifest.v1",
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "api_base": "https://www.klarefi.com/api/v1",
  "auth": {
    "scheme": "bearer",
    "header": "Authorization",
    "token_format": "{timestamp}.{hmacHex}",
    "token_source": "the token query parameter of the signed session URL",
    "session_expires_at": "2026-01-22T10:30:00.000Z"
  },
  "state_pointer": {
    "phase": "form",
    "processing_reason": null,
    "done_reason": null,
    "active_task_id": null,
    "submit_eligible": true,
    "submit_blocking_reasons": [],
    "events_cursor": 4,
    "under_review": false
  },
  "actions": [
    {
      "id": "get_state",
      "method": "GET",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000",
      "available": true
    },
    {
      "id": "save_draft",
      "method": "POST",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000/draft",
      "available": true,
      "when": "before submission"
    },
    {
      "id": "reply",
      "method": "POST",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000/replies",
      "available": false,
      "when": "after submission while the applicant phase is question",
      "blocked_reasons": [
        "replies are only accepted in question phase after submission"
      ]
    },
    {
      "id": "submit",
      "method": "POST",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000/submit",
      "available": true,
      "when": "before submission when submit_eligible is true"
    },
    {
      "id": "init_upload",
      "method": "POST",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000/uploads",
      "available": true
    },
    {
      "id": "complete_upload",
      "method": "POST",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000/uploads/{doc_id}/complete",
      "available": true
    },
    {
      "id": "poll_events",
      "method": "GET",
      "path": "/sessions/550e8400-e29b-41d4-a716-446655440000/events",
      "available": true,
      "when": "use after_cursor to resume from state_pointer.events_cursor"
    }
  ],
  "upload_policy": {
    "accepted_mime_types": [
      "application/pdf",
      "application/msword",
      "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "message/rfc822",
      "application/eml",
      "image/jpeg",
      "image/png",
      "image/webp",
      "image/tiff",
      "image/bmp",
      "video/mp4",
      "video/quicktime",
      "video/webm",
      "video/x-msvideo",
      "video/x-matroska",
      "text/plain"
    ],
    "max_file_size_bytes": 26214400
  },
  "polling": {
    "suggested_interval_ms": 3000,
    "mechanism": "cursor_events"
  },
  "docs_url": "https://www.klarefi.com/docs/guides/agent-intake"
}
```

Fetch the manifest once at the start of a run instead of hardcoding
capabilities. Availability changes as the session progresses.

### Discover from a session link

A web agent often starts from the signed session link itself, not from API
documentation. Session pages expose a token-free pointer for that case:

```http
GET /s/{sessionId}/agent.json
```

```json
{
  "schema_version": "hosted_session_agent_pointer.v1",
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "api_base": "https://www.klarefi.com/api/v1",
  "manifest_url": "https://www.klarefi.com/api/v1/sessions/550e8400-e29b-41d4-a716-446655440000/manifest",
  "auth": {
    "scheme": "bearer",
    "token_source": "the token query parameter of the signed session URL"
  },
  "docs_url": "https://www.klarefi.com/docs/guides/agent-intake"
}
```

The session page also carries a `rel="alternate"` link tag pointing at the
same pointer, so an agent that parses the HTML finds it without knowing the
URL shape.

The pointer carries no credentials. Its `api_base` and `manifest_url` are
derived from the configured public app URL, or from the request origin when no
public app URL is configured. An agent landing on a session link should fetch
the pointer, take the bearer token from the link's `token` query parameter, and
then fetch the manifest with that token to learn what the session supports.

## Idempotency

Every mutation is safe to retry:

- Replies accept `idempotency_key`. The convention is
  `reply:{task_id}:{source_event_sequence}`, which makes a retry of the same
  answer to the same question a no-op.
- Submit accepts `submission_id` and replays instead of double-submitting.
- Drafts carry `client_mutation_id` and uploads carry `upload_id` for the
  same reason.

## SDK quickstart

`@klarefi/node` ships an applicant-plane client that wraps the loop,
idempotency keys, and upload steps:

```bash
npm install @klarefi/node
```

```ts
import { IntakeSession } from "@klarefi/node/intake";

const intake = IntakeSession.fromSignedUrl(
  "https://app.klarefi.com/s/550e8400...?token=1751879000.9f2c4e...",
);

let state = await intake.getState();

while (state.next_action.type !== "none") {
  switch (state.next_action.type) {
    case "fill_form":
      state = await intake.saveDraft({ claimant_name: "Ada Lovelace" });
      state = (await intake.submit()).state;
      break;
    case "answer":
      state = await intake.answer("The claim number is CLM-123.");
      break;
    case "upload":
      state = await intake.uploadDocument({
        file: "./policy-card.pdf",
        mimeType: "application/pdf",
      });
      break;
    case "wait": {
      const result = await intake.nextAction({ timeoutMs: 60_000 });
      state = result.state;
      break;
    }
  }
}
```

You can also construct the client from parts:

```ts
const intake = new IntakeSession({
  sessionId: "550e8400-e29b-41d4-a716-446655440000",
  accessToken: process.env.INTAKE_SESSION_TOKEN!,
  baseUrl: process.env.KLAREFI_BASE_URL!,
  clientName: "claims-copilot",
});
```

The methods map onto the loop:

| Method                                | What it does                                                                                                                                         |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getState()`                          | Distilled `AgentIntakeState`: `phase`, a typed `next_action` (`fill_form`, `answer`, `upload`, `wait`, `none`), progress counts, and submit blockers |
| `answer(text, { taskId? })`           | Reply to the active question with free text                                                                                                          |
| `answerFields({ factId: value })`     | Reply with typed field values; validates against each task's response fields                                                                         |
| `saveDraft(fields)`                   | Save form field values without submitting                                                                                                            |
| `uploadDocument({ file, mimeType? })` | Runs initialize, transfer, and complete; accepts a path, `Uint8Array`, or `Blob`                                                                     |
| `submit()`                            | Returns `{ submitted, blockers, state }`; a `409 submit_blocked` becomes `submitted: false` with the blocking reasons                                |
| `nextAction({ timeoutMs })`           | Polls until the next action is ready or the timeout elapses                                                                                          |
| `getView()`                           | The raw hosted view when an agent needs the full escape hatch                                                                                        |

`nextAction()` polls every 2 seconds by default and respects the mutation it
follows: it waits until the session has actually moved past the version your
last write produced.

A form can ask some questions once for each of several things, such as each
property a loan is secured on. Each of those fields in `fill_form` has an id
like `property_value@elm_road`, and its label names the property:
"Property value (12 Elm Road)". Save its answer under that id. The applicant
adds or removes a property on the hosted form; an agent answers the fields
that are there.

## MCP server

Klarefi hosts a stateless MCP `2026-07-28` endpoint. It exposes the same loop
as MCP tools, so an agent runtime can drive an intake without custom code:

```text
https://www.klarefi.com/mcp
```

Use [Connect MCP](https://install.apicommons.org/?server=https%3A%2F%2Fwww.klarefi.com%2F.well-known%2Fmcp%2Fserver.json)
to select your client. The installer reads Klarefi's public server manifest.
It does not put an API key in the link.

When the client asks for the `Authorization` header, enter
`Bearer sk_test_...` or `Bearer sk_live_...`. Omit it when you only need
applicant tools. The signed intake link is the applicant credential.

Use the local package only when the client cannot connect to a remote MCP
URL:

```json
{
  "mcpServers": {
    "klarefi": {
      "command": "bunx",
      "args": ["--bun", "@klarefi/mcp"],
      "env": {
        "KLAREFI_API_KEY": "sk_test_..."
      }
    }
  }
}
```

The hosted MCP server keeps no protocol session. Include the signed intake
`url`, or `session_id` with `access_token`, in each applicant tool call. Local
stdio clients can call `open_intake` once and reuse it on that connection.

| Tool                            | Plane     | What it does                                                                                                 |
| ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| `open_intake`                   | Applicant | Open a session from a signed link                                                                            |
| `get_intake_state`              | Applicant | Read the distilled state and next action                                                                     |
| `answer_question`               | Applicant | Answer the active question with free text                                                                    |
| `answer_fields`                 | Applicant | Answer with typed field values                                                                               |
| `upload_document`               | Applicant | Upload a document to the active upload task                                                                  |
| `save_intake_draft`             | Applicant | Save form field values                                                                                       |
| `submit_intake`                 | Applicant | Submit; returns blockers if the session is not ready                                                         |
| `wait_for_next_action`          | Applicant | Poll until the next action is ready                                                                          |
| `create_intake_session`         | Customer  | Create a hosted session; requires `intake:sessions:create`                                                   |
| `get_case`                      | Customer  | Read the case projection; requires `cases:read`                                                              |
| `list_operator_queue`           | Customer  | Read the bounded queue; requires `cases:review`                                                              |
| `get_case_workspace`            | Customer  | Read the cited workspace; requires `cases:read`                                                              |
| `get_case_package`              | Customer  | Read the package and signed case-file link; requires `cases:read`                                            |
| `submit_case_review`            | Customer  | Submit an auditable review command; requires `cases:review`; cannot complete or reopen a case                |
| `get_review_command`            | Customer  | Poll a review command; requires `cases:read`                                                                 |
| `list_workflows`                | Customer  | Read workflow declarations; requires `workflows:read`                                                        |
| `save_workflow_draft`           | Customer  | Save a draft; requires `workflows:write`; cannot publish or activate it                                      |
| `create_connector_from_openapi` | Customer  | Import a connector; requires `connectors:write`; may stage it for approval                                   |
| `create_connector`              | Customer  | Create or update a connector; requires `connectors:write`; may stage the configuration for approval          |
| `delete_connector`              | Customer  | Delete a connector; requires `connectors:write`; may stage deletion with `approval_status: pending_deletion` |

The read tools do not mutate cases or workflows. `submit_case_review` is
limited to the review command set. Workflow publishing remains
human-controlled.

## Prefill and handoff

The customer plane lets your systems complete the parts they already know
before the applicant sees the session.

`POST /api/v1/sessions` accepts `prefill.field_values` with form field values
from your records. The applicant only completes what remains. Compliance
fields cannot be prefilled, and consent is not a field: the applicant gives it
before the form opens, on the hosted page or through their agent.

```http
POST /api/v1/sessions
Authorization: Bearer sk_live_...
Content-Type: application/json

{
  "case_type_id": "motor_claim",
  "idempotency_key": "session_claim_48392",
  "external_case_id": "claim_48392",
  "prefill": {
    "field_values": {
      "claimant_name": "Ada Lovelace",
      "policy_number": "POL-8842"
    }
  }
}
```

The response includes `signed_url`, an `access_token` for the applicant
plane, and an `agent` block describing the session endpoints, so your own
agent can act on the session directly.

When your side is done, mark the handoff:

```http
POST /api/v1/sessions/{sessionId}/handoff
Authorization: Bearer sk_live_...
Content-Type: application/json

{
  "notify": { "email": "ada@example.com" }
}
```

Handoff marks the customer-completed portion, returns a fresh signed link to
send to the applicant, and emits a `v1.intake.handoff_ready` webhook so your
delivery flow can pick it up. The webhook remains the default integration
path.

`notify` is optional. When present, Klarefi emails the applicant a continue
link directly, so a handoff works even without your own delivery flow.
Notification is fail-soft: if the email cannot be sent, the handoff still
succeeds. The response reports what happened in a `notification` block:

```json
{
  "notification": { "requested": true, "sent": true }
}
```

`requested` reflects whether the request asked for a notification, `sent`
whether Klarefi delivered one, and a `reason` field explains any `sent: false`.

Every agent-submitted answer is recorded with an actor and an optional
`client_name`, and shows up in the case file. Reviewers see who provided
each answer: your agent, the applicant's agent, or the applicant.

## Sessions that start with an email, and offers

A session that Klarefi opened because someone emailed your mailbox works
like any other for an agent. Its link is the one in Klarefi's reply. The
answers the email already gave are filled in after consent, so read the
session state before saving draft values. See
[Email intake](/docs/guides/email-intake).

Where a workflow ends with an offer, an agent's part ends when the intake is
complete. An agent cannot accept an offer: a person signs it on the offer
page, with a code sent to their email. See
[Offers and declines](/docs/guides/offers).

## Related guides

- [Create an intake session](/docs/guides/create-intake-session)
- [Hosted intake](/docs/guides/hosted-intake)
- [Webhook setup](/docs/guides/webhooks)
- [SDKs and CLI](/docs/api/sdks)
- [Idempotency](/docs/api/idempotency)
