# Agent intake
Let an agent complete a hosted intake session over the applicant API, the TypeScript SDK, or MCP
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 → Developer**, 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.

### 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 2
seconds, and respect `Retry-After` headers when the API sends them.

## Discover capabilities

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

```json
{
  "auth": { "scheme": "bearer", "scope": "session" },
  "actions": [
    { "name": "save_draft", "available": true },
    { "name": "reply", "available": true },
    { "name": "upload", "available": true },
    { "name": "submit", "available": false, "reason": "unresolved_follow_up" }
  ],
  "uploads": {
    "accepted_mime_types": ["application/pdf", "image/png", "image/jpeg"],
    "max_file_size_bytes": 26214400
  },
  "polling": { "min_interval_ms": 2000 }
}
```

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://your-deployment.convex.site/api/v1",
  "manifest_url": "https://your-deployment.convex.site/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 Convex deployment, so they remain correct even
when the public vanity domain is unavailable. 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" });
      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.

## 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                       |

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 and
consent fields cannot be prefilled; the applicant must provide those.

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

## 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)
