Agent intake
Let an agent complete a hosted intake session over the applicant API, the TypeScript SDK, or MCP
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.jsoncarries 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.
- Read state.
GET /api/v1/sessions/{sessionId}returns the current phase, the active task, and submit readiness. - Act. Save draft form values, answer the active question, upload a document, or submit.
- Wait. After a reply the session enters
processingwhile facts are resolved. Poll the events endpoint or re-read state until the next question is ready. - Repeat until the phase is
done.
Read session state
Authenticate every applicant-plane request with the session token as a bearer token:
GET /api/v1/sessions/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer 1751879000.9f2c4e...{
"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
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
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.
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:
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
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. Agap.promptedevent means a new question is ready. - Re-read
GET /api/v1/sessions/{sessionId}and checkstate_pointer.phase.
GET /api/v1/sessions/{sessionId}/events?after_cursor=14
Authorization: Bearer <token>{
"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:
{
"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:
GET /s/{sessionId}/agent.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 isreply:{task_id}:{source_event_sequence}, which makes a retry of the same answer to the same question a no-op. - Submit accepts
submission_idand replays instead of double-submitting. - Drafts carry
client_mutation_idand uploads carryupload_idfor the same reason.
SDK quickstart
@klarefi/node ships an applicant-plane client that wraps the loop,
idempotency keys, and upload steps:
npm install @klarefi/nodeimport { 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:
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/mcp exposes the same loop as MCP tools, so an agent runtime can
drive an intake without custom code:
{
"mcpServers": {
"klarefi": {
"command": "npx",
"args": ["-y", "@klarefi/mcp"],
"env": {
"KLAREFI_API_KEY": "sk_test_..."
}
}
}
}The applicant-plane tools need no API key; the signed session link passed to
open_intake is the credential. Set KLAREFI_API_KEY only if you also want
the customer-plane tools.
| 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 KLAREFI_API_KEY) |
get_case | Customer | Read a case and its resolved facts (requires KLAREFI_API_KEY) |
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.
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:
POST /api/v1/sessions/{sessionId}/handoff
Authorization: Bearer sk_live_...
Content-Type: application/json
{
"notify": { "email": "[email protected]" }
}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:
{
"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.