CUA Website Scan
curl --request POST \
--url https://api.example.com/kyc/cua-scanimport requests
url = "https://api.example.com/kyc/cua-scan"
response = requests.post(url)
print(response.text)const options = {method: 'POST'};
fetch('https://api.example.com/kyc/cua-scan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/kyc/cua-scan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/kyc/cua-scan"
req, _ := http.NewRequest("POST", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/kyc/cua-scan")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/kyc/cua-scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
response = http.request(request)
puts response.read_bodyAI Analysis
CUA Website Scan
POST
/
kyc
/
cua-scan
CUA Website Scan
curl --request POST \
--url https://api.example.com/kyc/cua-scanimport requests
url = "https://api.example.com/kyc/cua-scan"
response = requests.post(url)
print(response.text)const options = {method: 'POST'};
fetch('https://api.example.com/kyc/cua-scan', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/kyc/cua-scan",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/kyc/cua-scan"
req, _ := http.NewRequest("POST", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/kyc/cua-scan")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/kyc/cua-scan")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
response = http.request(request)
puts response.read_bodyAI-powered website analysis using Computer Use Agent (CUA) technology. The agent autonomously navigates and analyzes websites to identify potential fraud, scams, compliance issues, and suspicious content.
Overview
The CUA Scan API uses Claude’s Computer Use capabilities to:- Navigate websites like a human user
- Analyze content for fraud indicators, scams, and compliance issues
- Capture screenshots at each step for audit trails
- Generate detailed reports with findings and recommendations
CUA scans are asynchronous. Submit a job, then poll for status and results.
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/kyc/cua-scan | POST | Submit a new scan job |
/kyc/cua-scan | GET | List jobs with pagination |
/kyc/cua-scan/{jobId} | GET | Get job status and results |
Authentication
| Endpoint | Required Permission |
|---|---|
| Submit Job | cua:create |
| List Jobs | cua:read |
| Get Status | cua:read |
Submit Scan Job
Create a new CUA scan job to analyze a website.POST /kyc/cua-scan
Request Parameters
Effort Levels
| Level | Steps | Use Case |
|---|---|---|
low | 20 | Quick surface-level scan |
medium | 50 | Standard comprehensive scan (default) |
high | 80 | Deep analysis for complex sites |
Request Example
curl -X POST https://stg.kyc.legaltalent.ai/kyc/cua-scan \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://suspicious-shop.com",
"effort": "medium"
}'
Response
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "PENDING",
"target_url": "https://suspicious-shop.com",
"effort": "medium",
"created_at": "2024-12-15T10:30:00.000000+00:00",
"message": "Job submitted successfully"
}
Response Fields
| Field | Type | Description |
|---|---|---|
job_id | string | Unique job identifier (UUID) |
status | string | Initial status: PENDING |
target_url | string | URL being scanned |
effort | string | Effort level selected |
created_at | string | ISO 8601 timestamp |
message | string | Confirmation message |
List Jobs
Get a paginated list of scan jobs for your tenant.GET /kyc/cua-scan
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Number of items per page (max 100) |
cursor | string | - | Pagination cursor from previous response |
status | string | - | Filter by status: PENDING, PROCESSING, COMPLETED, FAILED |
sort | string | desc | Sort order: desc (newest first) or asc (oldest first) |
Request Example
# List recent jobs
curl -X GET "https://stg.kyc.legaltalent.ai/kyc/cua-scan?limit=10&sort=desc" \
-H "Authorization: Bearer YOUR_TOKEN"
# Filter by status
curl -X GET "https://stg.kyc.legaltalent.ai/kyc/cua-scan?status=COMPLETED&limit=20" \
-H "Authorization: Bearer YOUR_TOKEN"
# Paginate through results
curl -X GET "https://stg.kyc.legaltalent.ai/kyc/cua-scan?cursor=eyJqb2JfaWQiOiAiLi4uIn0=" \
-H "Authorization: Bearer YOUR_TOKEN"
Response
{
"items": [
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"target_url": "https://suspicious-shop.com",
"status": "COMPLETED",
"effort": "medium",
"created_at": "2024-12-15T10:30:00.000000+00:00",
"updated_at": "2024-12-15T10:35:00.000000+00:00"
},
{
"job_id": "660e8400-e29b-41d4-a716-446655440001",
"target_url": "https://another-site.com",
"status": "PROCESSING",
"effort": "high",
"created_at": "2024-12-15T10:32:00.000000+00:00",
"updated_at": "2024-12-15T10:33:00.000000+00:00"
}
],
"count": 2,
"cursor": "eyJqb2JfaWQiOiAiNjYwZTg0MDAuLi4ifQ==",
"has_more": true
}
Response Fields
| Field | Type | Description |
|---|---|---|
items | array | List of job summaries |
count | integer | Number of items in this response |
cursor | string | Pagination cursor for next page (null if no more) |
has_more | boolean | Whether more pages exist |
Get Job Status
Get detailed status, progress, and results of a scan job.GET /kyc/cua-scan/{jobId}
Path Parameters
| Parameter | Type | Description |
|---|---|---|
jobId | string | Job ID (UUID) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
from_step | integer | 0 | Return only steps with sequence_id >= from_step (for polling) |
Request Example
# Get full status
curl -X GET "https://stg.kyc.legaltalent.ai/kyc/cua-scan/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer YOUR_TOKEN"
# Poll for new steps (efficient for real-time updates)
curl -X GET "https://stg.kyc.legaltalent.ai/kyc/cua-scan/550e8400-e29b-41d4-a716-446655440000?from_step=15" \
-H "Authorization: Bearer YOUR_TOKEN"
Response - Processing
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"target_url": "https://suspicious-shop.com",
"status": "PROCESSING",
"effort": "medium",
"created_at": "2024-12-15T10:30:00.000000+00:00",
"updated_at": "2024-12-15T10:31:30.000000+00:00",
"current_step": 5,
"total_sequences": 12,
"last_sequence_id": 12,
"from_sequence": 0,
"steps": [
{
"sequence_id": 1,
"agent_step": 1,
"type": "text",
"content": "I'll analyze this website for potential fraud indicators..."
},
{
"sequence_id": 2,
"agent_step": 1,
"type": "tool_use",
"tool": "computer",
"action": "screenshot"
},
{
"sequence_id": 3,
"agent_step": 1,
"type": "tool_result",
"success": true,
"screenshot_url": "https://bucket.s3.amazonaws.com/screenshots/step1.png?..."
},
{
"sequence_id": 4,
"agent_step": 2,
"type": "text",
"content": "The homepage shows several red flags: no company address..."
}
]
}
Response - Completed
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"target_url": "https://suspicious-shop.com",
"status": "COMPLETED",
"effort": "medium",
"created_at": "2024-12-15T10:30:00.000000+00:00",
"updated_at": "2024-12-15T10:35:00.000000+00:00",
"current_step": 50,
"total_sequences": 145,
"last_sequence_id": 145,
"from_sequence": 0,
"steps": [...],
"results": {
"files_count": 3,
"files": [
{
"file_name": "report.json",
"file_key": "results/550e8400.../report.json",
"size": 15234,
"last_modified": "2024-12-15T10:35:00.000000+00:00",
"download_url": "https://bucket.s3.amazonaws.com/results/550e8400.../report.json?..."
},
{
"file_name": "summary.md",
"file_key": "results/550e8400.../summary.md",
"size": 4521,
"last_modified": "2024-12-15T10:35:00.000000+00:00",
"download_url": "https://bucket.s3.amazonaws.com/results/550e8400.../summary.md?..."
},
{
"file_name": "screenshots.zip",
"file_key": "results/550e8400.../screenshots.zip",
"size": 2548000,
"last_modified": "2024-12-15T10:35:00.000000+00:00",
"download_url": "https://bucket.s3.amazonaws.com/results/550e8400.../screenshots.zip?..."
}
],
"s3_location": "s3://bucket-name/results/550e8400.../"
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
job_id | string | Job ID |
target_url | string | URL being scanned |
status | string | Current status |
effort | string | Effort level |
created_at | string | Job creation timestamp |
updated_at | string | Last update timestamp |
current_step | integer | Current agent step (1 to max_steps) |
total_sequences | integer | Total content blocks generated |
last_sequence_id | integer | ID of last sequence (for pagination) |
from_sequence | integer | Requested from_step value |
steps | array | Content blocks (text, tool_use, tool_result) |
results | object | Result files (only when COMPLETED) |
error_message | string | Error details (only when FAILED) |
Step Types
| Type | Description |
|---|---|
text | Agent’s reasoning/analysis text |
tool_use | Tool invocation (screenshot, click, type, etc.) |
tool_result | Result of tool execution |
thinking | Internal reasoning (when available) |
Job Statuses
| Status | Description |
|---|---|
PENDING | Job submitted, waiting in queue |
PROCESSING | Agent is actively scanning the website |
COMPLETED | Scan finished successfully, results available |
FAILED | Scan failed (see error_message for details) |
Error Responses
400 Bad Request - Missing URL
{
"error": "Missing required field: url"
}
400 Bad Request - Invalid URL
{
"error": "URL must start with http:// or https://"
}
400 Bad Request - Invalid Effort
{
"error": "Invalid effort level: extreme. Valid options are: low, medium, high"
}
403 Forbidden - Permission Denied
{
"error": "Permission denied",
"message": "Required permission: cua:create"
}
404 Not Found
{
"error": "Job not found"
}
Usage Examples
Python - Complete Workflow
import requests
import time
BASE_URL = "https://stg.kyc.legaltalent.ai"
TOKEN = "YOUR_TOKEN"
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"
}
def submit_scan(url: str, effort: str = "medium") -> dict:
"""Submit a new CUA scan job."""
response = requests.post(
f"{BASE_URL}/kyc/cua-scan",
headers=headers,
json={"url": url, "effort": effort}
)
response.raise_for_status()
return response.json()
def get_status(job_id: str, from_step: int = 0) -> dict:
"""Get job status with optional pagination."""
params = {"from_step": from_step} if from_step > 0 else {}
response = requests.get(
f"{BASE_URL}/kyc/cua-scan/{job_id}",
headers=headers,
params=params
)
response.raise_for_status()
return response.json()
def poll_until_complete(job_id: str, interval: int = 5) -> dict:
"""Poll job status until completed or failed."""
last_sequence = 0
while True:
status = get_status(job_id, from_step=last_sequence)
print(f"Status: {status['status']}, Step: {status['current_step']}")
# Print new steps
for step in status.get('steps', []):
if step['type'] == 'text':
print(f" [{step['agent_step']}] {step['content'][:100]}...")
if status['status'] == 'COMPLETED':
print(f"\n✅ Scan completed! {status['results']['files_count']} result files available.")
return status
elif status['status'] == 'FAILED':
print(f"\n❌ Scan failed: {status.get('error_message', 'Unknown error')}")
return status
# Update pagination cursor
last_sequence = status.get('last_sequence_id', 0) + 1
time.sleep(interval)
# Example usage
if __name__ == "__main__":
# Submit scan
job = submit_scan("https://suspicious-shop.com", effort="medium")
print(f"Job submitted: {job['job_id']}")
# Poll until complete
result = poll_until_complete(job['job_id'])
# Download results
if result['status'] == 'COMPLETED':
for file in result['results']['files']:
print(f"Download: {file['file_name']} -> {file['download_url']}")
JavaScript - Real-time Updates
const BASE_URL = 'https://stg.kyc.legaltalent.ai';
const TOKEN = 'YOUR_TOKEN';
const headers = {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': 'application/json'
};
async function submitScan(url, effort = 'medium') {
const response = await fetch(`${BASE_URL}/kyc/cua-scan`, {
method: 'POST',
headers,
body: JSON.stringify({ url, effort })
});
return response.json();
}
async function getStatus(jobId, fromStep = 0) {
const url = fromStep > 0
? `${BASE_URL}/kyc/cua-scan/${jobId}?from_step=${fromStep}`
: `${BASE_URL}/kyc/cua-scan/${jobId}`;
const response = await fetch(url, { headers });
return response.json();
}
async function pollWithCallback(jobId, onUpdate, interval = 3000) {
let lastSequence = 0;
const poll = async () => {
const status = await getStatus(jobId, lastSequence);
// Notify callback with new data
onUpdate(status);
if (status.status === 'COMPLETED' || status.status === 'FAILED') {
return status;
}
// Update cursor for next poll
lastSequence = (status.last_sequence_id || 0) + 1;
// Schedule next poll
await new Promise(resolve => setTimeout(resolve, interval));
return poll();
};
return poll();
}
// Example: Real-time UI updates
async function scanWithLiveUpdates(url) {
const job = await submitScan(url, 'medium');
console.log(`Job started: ${job.job_id}`);
const result = await pollWithCallback(job.job_id, (status) => {
console.log(`[${status.status}] Step ${status.current_step}`);
// Update UI with new steps
status.steps?.forEach(step => {
if (step.type === 'text') {
console.log(` Agent: ${step.content.slice(0, 80)}...`);
} else if (step.type === 'tool_result' && step.screenshot_url) {
console.log(` 📸 Screenshot: ${step.screenshot_url}`);
}
});
});
if (result.status === 'COMPLETED') {
console.log('\n✅ Scan complete!');
console.log('Results:', result.results.files.map(f => f.file_name));
}
return result;
}
List Jobs with Filtering
def list_jobs(status=None, limit=20, sort='desc'):
"""List scan jobs with optional filtering."""
params = {'limit': limit, 'sort': sort}
if status:
params['status'] = status
response = requests.get(
f"{BASE_URL}/kyc/cua-scan",
headers=headers,
params=params
)
return response.json()
def list_all_jobs(status=None):
"""Iterate through all jobs using pagination."""
cursor = None
all_jobs = []
while True:
params = {'limit': 100}
if status:
params['status'] = status
if cursor:
params['cursor'] = cursor
response = requests.get(
f"{BASE_URL}/kyc/cua-scan",
headers=headers,
params=params
)
data = response.json()
all_jobs.extend(data['items'])
if not data['has_more']:
break
cursor = data['cursor']
return all_jobs
# Get all completed jobs
completed_jobs = list_all_jobs(status='COMPLETED')
print(f"Total completed scans: {len(completed_jobs)}")
Best Practices
Choosing Effort Level
- Low (20 steps): Quick checks, simple landing pages
- Medium (50 steps): Standard analysis, e-commerce sites
- High (80 steps): Deep investigation, complex sites with multiple pages
Efficient Polling
- Use
from_stepparameter to only fetch new content - Poll every 3-5 seconds during active processing
- Stop polling when status is
COMPLETEDorFAILED
Rate Limiting
- Maximum 2 concurrent jobs per tenant
- Maximum 10 concurrent jobs globally
- Jobs exceeding limits are queued automatically
Result Files
- report.json: Structured analysis data
- summary.md: Human-readable summary
- screenshots.zip: All captured screenshots
- Download URLs expire after 1 hour
Error Handling
- Implement retry logic for transient failures
- Check
error_messagefield when status isFAILED - Store
job_idfor later retrieval of results
⌘I