Skip to main content

Scan API

Tiered entity scanning endpoints for automated KYB compliance checks. The scan system provides four depth levels, from instant graph-based risk scoring (Tier 0) to full compliance case investigation (Tier 3).

All scan endpoints require officer authentication and are under the /api prefix.

Tier Overview

TierNameLLM CallsLatencyCostDescription
0E-VAL0<100ms0cGraph-only risk score from cross-investigation signals
1Lightweight Scan02-8s1cKBO registry + PEPPOL verification + sanctions screening
2Standard Scan110-20s2-6cTier 1 + adverse media search + LLM synthesis
3Full Investigation5+60-160s--Creates a full compliance case with Temporal workflow

Endpoints Summary

MethodPathPurpose
POST/api/scan/entity/{reg_number}Scan a single entity at a specified tier
GET/api/scan/entity/{reg_number}/resultsGet scan history for an entity
POST/api/scan/entity/{reg_number}/escalateEscalate to a higher scan tier
POST/api/scan/portfolioBatch scan a portfolio of entities
GET/api/scan/portfolio/{portfolio_id}/resultsGet results for a portfolio scan

Scan Entity

POST /api/scan/entity/{reg_number}

Scans a single entity at the specified tier depth. Tiers 0-2 return results synchronously. Tier 3 creates a full compliance case and starts a Temporal workflow.

Path Parameters

ParameterTypeDescription
reg_numberstringBelgian enterprise/registration number (e.g., 0456789012)

Request Body

{
"tier": 1,
"segment_id": "psp-merchants-eu"
}
FieldTypeRequiredDefaultDescription
tierintegerNo1Scan depth: 0 (E-VAL), 1 (lightweight), 2 (standard), 3 (full investigation)
segment_idstringNonullOptional segment identifier for portfolio grouping

Tier 0 Response

Response 200

{
"registration_number": "0456789012",
"eval_score": 0.15,
"graph_degree": 3,
"cross_investigation_count": 1,
"above_threshold": false
}
FieldTypeDescription
registration_numberstringThe queried registration number
eval_scorefloatRisk score from 0.0 (safe) to 1.0 (high risk), derived from graph signals
graph_degreeintegerNumber of graph connections for this entity
cross_investigation_countintegerNumber of other investigations that have touched this entity
above_thresholdbooleanWhether eval_score exceeds the configured threshold (default 0.3)

Tier 1 Response

Response 200

{
"scan_id": "scan-0456789012-t1-20260227143022",
"registration_number": "0456789012",
"tier": 1,
"risk_tier": "green",
"confidence": 0.8,
"eval_score": 0.15,
"company_status": "active",
"legal_name": "Acme Trading BVBA",
"nace_codes": ["47.19", "62.01"],
"director_count": 2,
"ubo_count": 0,
"sanctions_exact_matches": 0,
"sanctions_fuzzy_matches": 0,
"peppol_registered": true,
"withholding_obligations": false,
"tax_debt_detected": false,
"social_debt_detected": false,
"adverse_media_hits": 0,
"adverse_media_summary": "",
"synthesis_summary": "",
"flags": [],
"scan_cost_cents": 1,
"scanned_at": "2026-02-27T14:30:22Z",
"cached": false
}

Tier 2 Response

Response 200

Returns the same ScanResult schema as Tier 1, with additional fields populated:

{
"scan_id": "scan-0456789012-t2-20260227143055",
"registration_number": "0456789012",
"tier": 2,
"risk_tier": "amber",
"confidence": 0.9,
"eval_score": 0.15,
"company_status": "active",
"legal_name": "Acme Trading BVBA",
"nace_codes": ["47.19", "62.01"],
"director_count": 2,
"ubo_count": 0,
"sanctions_exact_matches": 0,
"sanctions_fuzzy_matches": 1,
"peppol_registered": true,
"withholding_obligations": false,
"tax_debt_detected": false,
"social_debt_detected": false,
"adverse_media_hits": 3,
"adverse_media_summary": "Found 3 articles mentioning regulatory inquiries in 2025.",
"synthesis_summary": "Entity shows moderate risk due to adverse media coverage. KBO and PEPPOL checks passed. Recommend Tier 3 investigation for regulatory clarity.",
"flags": ["SANCTIONS_FUZZY", "ADVERSE_MEDIA_FOUND"],
"scan_cost_cents": 6,
"scanned_at": "2026-02-27T14:30:55Z",
"cached": false
}

Key differences from Tier 1:

  • adverse_media_hits and adverse_media_summary are populated from media search
  • synthesis_summary contains an LLM-generated risk narrative
  • sanctions_fuzzy_matches may be refined via LLM resolution of ambiguous matches
  • confidence is typically higher (+0.1 over Tier 1)
  • scan_cost_cents is higher (2c base + 3c if adverse media hits found)

Tier 3 Response

Response 200

Tier 3 creates a full compliance case with a Temporal workflow. The response is a case creation payload, not a ScanResult.

{
"case_id": "case_a1b2c3d4e5f6",
"workflow_id": "wf_f6e5d4c3b2a1",
"portal_token": "pt_1234567890abcdef",
"portal_url": "http://localhost:3001/portal/pt_1234567890abcdef",
"company_name": "Acme Trading BVBA",
"registration_number": "0456789012",
"status": "CREATED",
"created_at": "2026-02-27T14:31:00Z"
}

The created case follows the standard Case Management lifecycle. The case's additional_data will include {"escalated_from_scan": true}.

ScanResult Schema

Full schema for Tier 1 and Tier 2 responses:

FieldTypeDescription
scan_idstringUnique scan identifier (format: scan-{reg}-t{tier}-{timestamp})
registration_numberstringThe scanned entity's registration number
tierintegerScan tier that was executed (1 or 2)
risk_tierstringOverall risk classification: green, amber, or red
confidencefloatConfidence score (0.0-1.0) based on data availability
eval_scorefloatGraph-based E-VAL risk score (0.0-1.0)
company_statusstringKBO company status (e.g., active, ceased, bankrupt)
legal_namestringLegal company name from KBO registry
nace_codesstring[]NACE activity codes from KBO
director_countintegerNumber of directors found in KBO
ubo_countintegerNumber of UBOs identified
sanctions_exact_matchesintegerExact sanctions list matches
sanctions_fuzzy_matchesintegerFuzzy/partial sanctions matches
peppol_registeredbooleanWhether entity is registered in PEPPOL directory
withholding_obligationsbooleanWhether withholding obligations exist (from inhoudingsplicht)
tax_debt_detectedbooleanTax debt flag from PEPPOL/inhoudingsplicht check
social_debt_detectedbooleanSocial security debt flag
adverse_media_hitsintegerNumber of adverse media articles found (Tier 2 only)
adverse_media_summarystringSummary of adverse media findings (Tier 2 only)
synthesis_summarystringLLM-generated risk narrative (Tier 2 only)
flagsstring[]Risk flags raised during scan (see below)
scan_cost_centsintegerEstimated cost of this scan in cents
scanned_atdatetimeISO 8601 timestamp of scan execution
cachedbooleanWhether result was served from cache

Risk Flags

FlagMeaning
KBO_UNAVAILABLEKBO registry lookup failed or returned no data
PEPPOL_UNAVAILABLEPEPPOL verification service unreachable
SANCTIONS_HITExact match found on sanctions list (sets risk_tier to red)
SANCTIONS_FUZZYFuzzy match found on sanctions list (sets risk_tier to amber)
WITHHOLDING_OBLIGATIONSTax or social security debt detected
COMPANY_INACTIVECompany status indicates ceased, dissolved, or bankrupt
ADVERSE_MEDIA_FOUNDAdverse media articles found (Tier 2 only)
ADVERSE_MEDIA_UNAVAILABLEAdverse media search failed (Tier 2 only)

Example: Tier 1 scan

curl -X POST http://localhost:8002/api/scan/entity/0456789012 \
-H "Content-Type: application/json" \
-d '{"tier": 1}'

Example: Tier 0 E-VAL score

curl -X POST http://localhost:8002/api/scan/entity/0456789012 \
-H "Content-Type: application/json" \
-d '{"tier": 0}'

Example: Tier 2 standard scan

curl -X POST http://localhost:8002/api/scan/entity/0456789012 \
-H "Content-Type: application/json" \
-d '{"tier": 2}'

Example: Tier 3 full investigation

curl -X POST http://localhost:8002/api/scan/entity/0456789012 \
-H "Content-Type: application/json" \
-d '{"tier": 3}'

Get Scan Results

GET /api/scan/entity/{reg_number}/results

Returns the scan history for an entity, retrieved from the graph database. Results are ordered by scan date, with the most recent first. Includes results from all tiers.

Path Parameters

ParameterTypeDescription
reg_numberstringBelgian enterprise/registration number

Response 200

[
{
"scan_id": "scan-0456789012-t2-20260227143055",
"registration_number": "0456789012",
"tier": 2,
"risk_tier": "amber",
"confidence": 0.9,
"eval_score": 0.15,
"scanned_at": "2026-02-27T14:30:55Z",
"flags": ["SANCTIONS_FUZZY", "ADVERSE_MEDIA_FOUND"],
"scan_cost_cents": 6
},
{
"scan_id": "scan-0456789012-t1-20260225100000",
"registration_number": "0456789012",
"tier": 1,
"risk_tier": "green",
"confidence": 0.8,
"eval_score": 0.1,
"scanned_at": "2026-02-25T10:00:00Z",
"flags": [],
"scan_cost_cents": 1
}
]

Returns an empty array [] if no scans exist for the entity.

Example

curl http://localhost:8002/api/scan/entity/0456789012/results

Escalate Scan

POST /api/scan/entity/{reg_number}/escalate

Escalates an entity to a higher scan tier. Typically used when a lower-tier scan reveals risk signals that warrant deeper investigation. Tier 1 escalation forces a fresh scan (bypasses cache).

Path Parameters

ParameterTypeDescription
reg_numberstringBelgian enterprise/registration number

Request Body

{
"target_tier": 2
}
FieldTypeRequiredDescription
target_tierintegerYesTarget tier to escalate to: 1, 2, or 3

Response 200

  • Target tier 1: Returns a ScanResult (fresh, bypasses cache)
  • Target tier 2: Returns a ScanResult with adverse media and synthesis
  • Target tier 3: Returns a case creation payload (same as Tier 3 in Scan Entity)

Example: Escalate to Tier 2

curl -X POST http://localhost:8002/api/scan/entity/0456789012/escalate \
-H "Content-Type: application/json" \
-d '{"target_tier": 2}'

Example: Escalate to full investigation

curl -X POST http://localhost:8002/api/scan/entity/0456789012/escalate \
-H "Content-Type: application/json" \
-d '{"target_tier": 3}'
info

Escalating to Tier 3 creates a full compliance case with a Temporal workflow. This is a significant action -- the entity will appear in the officer dashboard as a new case requiring document collection and review.


Scan Portfolio

POST /api/scan/portfolio

Submits a batch of entities for parallel Tier 1 scanning. Entities are scanned concurrently (up to 20 in parallel) with rate limiting. Results are persisted to the graph database and linked to a portfolio node.

Request Body

{
"name": "Q1 2026 PSP Merchants",
"registration_numbers": [
"0456789012",
"0567890123",
"0678901234"
],
"segment_id": "psp-merchants-eu"
}
FieldTypeRequiredDescription
namestringYesHuman-readable portfolio name
registration_numbersstring[]YesList of registration numbers to scan
segment_idstringNoOptional segment identifier for categorization

Response 200

{
"portfolio_id": "portfolio-a1b2c3d4e5f6",
"portfolio_name": "Q1 2026 PSP Merchants",
"total_entities": 3,
"scanned": 3,
"failed": 0,
"summary": {
"green": 2,
"amber": 1,
"red": 0
},
"results": [
{
"scan_id": "scan-0456789012-t1-20260227143022",
"registration_number": "0456789012",
"tier": 1,
"risk_tier": "green",
"confidence": 0.8,
"eval_score": 0.1,
"company_status": "active",
"legal_name": "Acme Trading BVBA",
"nace_codes": ["47.19"],
"director_count": 2,
"ubo_count": 0,
"sanctions_exact_matches": 0,
"sanctions_fuzzy_matches": 0,
"peppol_registered": true,
"withholding_obligations": false,
"tax_debt_detected": false,
"social_debt_detected": false,
"adverse_media_hits": 0,
"adverse_media_summary": "",
"synthesis_summary": "",
"flags": [],
"scan_cost_cents": 1,
"scanned_at": "2026-02-27T14:30:22Z",
"cached": false
}
]
}
FieldTypeDescription
portfolio_idstringUnique portfolio identifier (format: portfolio-{hex})
portfolio_namestringThe submitted portfolio name
total_entitiesintegerTotal number of registration numbers submitted
scannedintegerNumber of entities successfully scanned
failedintegerNumber of entities that failed to scan
summaryobjectRisk tier distribution: counts of green, amber, and red
resultsScanResult[]Array of individual scan results (Tier 1 schema)

Example

curl -X POST http://localhost:8002/api/scan/portfolio \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 2026 PSP Merchants",
"registration_numbers": ["0456789012", "0567890123", "0678901234"],
"segment_id": "psp-merchants-eu"
}'
info

Portfolio scans always run at Tier 1. Entities flagged as amber or red can be individually escalated to higher tiers using the Escalate Scan endpoint.


Get Portfolio Results

GET /api/scan/portfolio/{portfolio_id}/results

Returns the scan results for all entities in a portfolio, retrieved from the graph database. This is useful for retrieving results after the initial batch scan or for checking updated results after individual entity escalations.

Path Parameters

ParameterTypeDescription
portfolio_idstringPortfolio identifier (returned by the Scan Portfolio endpoint)

Response 200

{
"portfolio_id": "portfolio-a1b2c3d4e5f6",
"results": [
{
"scan_id": "scan-0456789012-t1-20260227143022",
"registration_number": "0456789012",
"tier": 1,
"risk_tier": "green",
"scanned_at": "2026-02-27T14:30:22Z"
},
{
"scan_id": "scan-0567890123-t1-20260227143023",
"registration_number": "0567890123",
"tier": 1,
"risk_tier": "amber",
"scanned_at": "2026-02-27T14:30:23Z"
}
]
}

Returns an empty results array if the portfolio has no scan results.

Example

curl http://localhost:8002/api/scan/portfolio/portfolio-a1b2c3d4e5f6/results