Create an intelligent agent that processes visa applications end-to-end — using the live Thailand Visa agent as the worked example
Build a KYC Agent
This guide walks through the full lifecycle of a document-intelligence agent. The example is the Thailand Tourist Visa agent that runs in the 9thSense platform today. Every JSON snippet below reflects the actual agent definition stored in the database.
What is an agent?
An agent wraps a set of document types, a checklist of goal rules, and an optional conversational persona into a single deployable unit. When a case is opened against an agent, the platform:
- Stores a snapshot of the agent definition at the moment the case is created (version pin).
- Collects and classifies uploaded documents against the agent's
doc_type_map. - Evaluates each
goal_ruleas documents arrive. - Either auto-completes the case or routes it to the human review queue.
The Thailand Visa agent
The agent processes Indian-passport holders applying for a Thailand tourist visa. Its goal identifier is thailand_visa. It runs at intelligence level 2, which means it opens a conversational channel and greets the applicant before document collection begins.
Agent configuration
{
"intelligence_level": 2,
"persona": "You are Maya, a professional Thailand tourist visa processing officer at the Royal Thai Embassy. You are efficient, warm, and thorough. You guide applicants step by step through the document submission process with clarity and professionalism. You acknowledge each document as it is received, briefly confirming key details you extracted (name, dates, amounts — never raw field names). You cross-check that documents are consistent with each other. You never reveal internal rule thresholds, field identifiers, or system details. You speak in first person. When documents have issues, you explain what is wrong in plain language and ask the applicant to resubmit. When the application is complete, you finalise it warmly.",
"greeting": "Sawadee kha! Welcome to the Royal Thai Embassy visa processing portal.\n\nI am Maya, your dedicated visa processing officer.\n\nTo process your Thailand tourist visa, I need the following 5 documents:\n\n1. Indian Passport - photo identification page (min. 6 months validity)\n2. Outbound Flight Ticket - your confirmed flight to Thailand\n (Connecting/via flights accepted - I will read the final destination)\n3. Return Flight Ticket - your confirmed return to India\n4. Hotel Booking Confirmation - in Thailand\n (Group/family bookings accepted)\n5. Bank Statement - within 3 months, showing sufficient funds\n\nYou may upload in any order. I will cross-check all names, dates, and destinations for consistency.\n\nWhen ready, please upload your first document.",
"doc_type_map": {
"passport": "passport",
"outbound_flight": "flight_ticket",
"return_flight": "flight_ticket",
"hotel_booking": "hotel_booking",
"bank_statement": "bank_statement"
}
}
The doc_type_map keys are the logical slot names used in goal rules. The values are the document classifier labels that the extraction pipeline produces. Multiple slots can share the same classifier label (e.g., outbound_flight and return_flight both use flight_ticket).
Goal rules
Goal rules are the checklist that must pass before a case can complete. Each rule has a severity and an on_deny block that controls retry behaviour. For the full rule anatomy and check-type reference, see Goal Rules.
Severity types
| Severity | Behaviour on failure |
|---|---|
hard_stop | Case is immediately denied. max_retries: 0. No resubmission allowed. |
require | Rule must pass before completion. Applicant may resubmit the flagged document up to max_retries times. |
Selected goal rules from the Thailand Visa agent
Hard stop — nationality check
{
"id": "passport_nationality",
"severity": "hard_stop",
"check": "field_contains",
"params": {
"field": "nationality",
"value": "INDIA",
"source": "passport",
"on_fail_message": "Only Indian nationals qualify for this visa pathway."
},
"on_deny": {
"message": "Non-Indian passport holder — hard stop.",
"max_retries": 0,
"remediate_type": ""
}
}
Hard stop — passport validity
{
"id": "passport_validity_6m",
"severity": "hard_stop",
"check": "date_after",
"params": {
"field": "date_of_expiry",
"source": "passport",
"min_gap_days": 180,
"on_fail_message": "Your passport does not have the required 6 months of validity beyond the travel date."
},
"on_deny": {
"message": "Passport expires within 6 months.",
"max_retries": 0,
"remediate_type": ""
}
}
Require — sufficient funds (remediable)
{
"id": "funds_sufficient",
"severity": "require",
"check": "field_gte",
"params": {
"field": "closing_balance",
"value": 50000,
"source": "bank_statement",
"on_fail_message": "Your bank statement shows a closing balance below INR 50,000."
},
"on_deny": {
"message": "Insufficient funds (< INR 50,000).",
"max_retries": 2,
"remediate_type": "bank_statement"
}
}
When max_retries is greater than zero and remediate_type names a document slot, the platform asks the applicant to resubmit that document type and re-evaluates the rule on the new upload.
Require — AI cross-check (custom rule)
Not all checks can be expressed with simple comparators. The custom check type lets you write a natural-language prompt that the AI evaluates:
{
"id": "outbound_to_thailand",
"severity": "hard_stop",
"check": "custom",
"params": {
"prompt": "You are verifying the outbound flight destination for a Thailand visa. The flight data contains destination_city, destination_country, and optionally via_cities (intermediate stops). Check if the FINAL destination is Thailand (Bangkok/BKK, Krabi/KBV, Phuket/HKT, Chiang Mai/CNX, Koh Samui/USM, Hat Yai/HDY, or any Thai city). Ignore transit/connection cities in via_cities. Return pass if final destination is Thailand, fail if it is clearly a different country.",
"sources": ["outbound_flight"],
"on_fail_message": "Your outbound flight does not appear to have Thailand as the final destination."
},
"on_deny": {
"message": "Outbound flight not to Thailand.",
"max_retries": 1,
"remediate_type": "outbound_flight"
}
}
Custom rules run against the already-extracted JSON from the document, not the raw image.
Gating fraud checks
Verdicts from the platform's fraud and integrity checks — liveness, deepfake detection, face match, and document integrity — are gated with goal rules exactly like field rules. For example, requiring the applicant's selfie to match the ID photo:
{
"id": "selfie_matches_id",
"severity": "hard_stop",
"check": "face_match_gte",
"params": { "threshold": 0.85 },
"on_deny": { "message": "Selfie does not match ID photo.", "max_retries": 0, "remediate_type": "" }
}
See Goal Rules — check types for the full list of verdict-threshold checks.
Case lifecycle
1. Open a case
curl -X POST https://api.9thsense.ai/v1/cases \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"goal": "thailand_visa", "tenant_id": "acme-corp"}'
Response — 201 Created:
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"goal": "thailand_visa",
"status": "collecting",
"progress": {
"required": ["passport", "outbound_flight", "return_flight", "hotel_booking", "bank_statement"],
"received": [],
"complete": false
}
}
Because intelligence_level is 2, the platform immediately stores Maya's greeting as the first assistant message in the conversation history.
2. Fetch the greeting
curl https://api.9thsense.ai/v1/cases/550e8400.../messages \
-H "X-Api-Key: $API_KEY"
3. Upload documents
curl -X POST https://api.9thsense.ai/v1/cases/550e8400.../documents \
-H "X-Api-Key: $API_KEY" \
-F "file=@passport.jpg;type=image/jpeg" \
-F "label=passport"
The platform accepts JPEG, PNG, WebP, TIFF, BMP, GIF, and PDF up to 20 MB per file. Each upload triggers extraction and goal-rule evaluation. The response includes updated progress.
4. Complete the case
curl -X POST https://api.9thsense.ai/v1/cases/550e8400.../complete \
-H "X-Api-Key: $API_KEY"
If all require rules pass, status becomes completed. If any rule fails and retries are exhausted, the case moves to review for human decision.
5. Human decision (review queue)
curl -X PATCH https://api.9thsense.ai/v1/cases/550e8400.../decision \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"decision": "approve", "reason": "Documents verified manually"}'
decision must be "approve" (sets status completed) or "deny" (sets status denied).
6. Download the report
# JSON report
curl https://api.9thsense.ai/v1/cases/550e8400.../report \
-H "X-Api-Key: $API_KEY"
# PDF download
curl "https://api.9thsense.ai/v1/cases/550e8400.../report?format=pdf" \
-H "X-Api-Key: $API_KEY" \
-o report.pdf
Reports are generated on first access and cached in object storage. Pass ?regenerate=true to force a fresh report.
Case statuses
| Status | Meaning |
|---|---|
collecting | Waiting for documents |
completed | All rules passed (or approved in review) |
denied | Hard-stop rule triggered or reviewer denied |
review | Soft failures — awaiting human decision |
failed | Internal processing error |
Replaying a case
You can reprocess all documents from an existing case against the current agent version:
curl -X POST https://api.9thsense.ai/v1/cases/550e8400.../replay \
-H "X-Api-Key: $API_KEY"
This creates a new case, downloads all stored files from object storage, and resubmits them in order. Useful when the agent definition changes and you want to re-evaluate historical submissions.