Skip to main content
This guide helps you migrate an existing integration from the legacy KYC screening endpoint to the current v2 API. The v2 API keeps the same screening engine and list coverage, but standardizes the request/response envelope, authentication, and identifier matching so integrations are simpler and more predictable.
The screening logic, watchlists, and matching quality are the same across both versions. Migration is almost entirely about how you authenticate and how you read the response — the request body is largely backward compatible.

Why migrate

Standard response envelope

Every endpoint now returns a consistent status / data / meta structure, so error handling and parsing are uniform across the platform.

Unified authentication

A single Authorization: Bearer scheme (API key or JWT) replaces the legacy per-endpoint auth, with role-based permissions.

Value-first identifier matching

Document and ID matching now keys on the value, not the document-type label — you no longer need to send the exact document_type to get an exact ID hit.

Multi-list in one call

Screen against many lists in a single request with a consolidated summary and per-list results.

At a glance

AspectLegacyv2
Base URLLegacy gateway hosthttps://kyc.legaltalent.ai (prod) · https://stg.kyc.legaltalent.ai (staging)
Auth headerLegacy credentialAuthorization: Bearer <api_key | jwt>
EndpointLegacy screening pathPOST /kyc
Response envelopeJob object at the top level (job_id, status, …){ "status", "data", "meta" }
Request identifierjob_id in bodymeta.request_id in response
Top-level status"COMPLETED" / "FAILED""success" / "error"
Per-list match flaghas_matchesis_match
Matched entitymatches[].matched_entitymatches[].entity (single list) / matches[].match_data
Per-match riskmatches[].risk_level + confidence_scorematches[].confidence_score (derive level from score, or use summary.overall_risk_level)
Aggregate riskscreening_summarydata.summary

Migration in 5 steps

1

Switch the base URL and endpoint

Point your client at https://stg.kyc.legaltalent.ai/kyc for integration testing, then https://kyc.legaltalent.ai/kyc for production. The method stays POST.
2

Switch to Bearer authentication

Replace the legacy credential with an Authorization: Bearer header using your API key (sk_...) or a JWT. See Authentication. You no longer pass tenant_id in the body — it is derived from the token.
3

Keep your request body (with minor tweaks)

The subject object is backward compatible. If you used match_type, send search_type instead (both accept composite, exact, fuzzy, token, llm_enhanced).
4

Update your response parser

Read results from the new envelope: data for results, meta.request_id for the correlation id, data.summary for the aggregate risk. See the field mapping below.
5

Re-point your risk/decision logic

Use summary.overall_risk_level + summary.recommended_action for the aggregate decision, and matches[].confidence_score for per-match intensity. If you relied on per-match risk_level, derive it from confidence_score (see Risk and decisioning).

Authentication

The legacy integration used a dedicated credential on the legacy gateway. The v2 API uses a single Bearer scheme for both user (JWT) and machine-to-machine (API key) access.
curl -X POST https://kyc.legaltalent.ai/kyc \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "subject": { "full_name": "John Doe" }, "list_name": "ofac" }'
POST /kyc requires the kyc:create permission. The token also scopes the request to your tenant, so remove any tenant_id you previously sent in the request body.

Request changes

The request body is largely unchanged. The main adjustments:
Legacy fieldv2 fieldNotes
match_typesearch_typeSame accepted values; search_type is the canonical name.
tenant_id (body)Removed. Derived from the Bearer token.
subject.*subject.*Backward compatible (see below).

Identifier matching improvement

In v2, identifier matching is value-first: an exact ID/document number matches a list entry regardless of how the document-type label is named on either side (CI, DNI, National ID, document_id, numeric id, etc.). Values are normalized (non-alphanumeric characters stripped, case-insensitive) before comparison.
Country (nationality) and birth_date behave the same as before: they refine name-based matches (and can lower confidence on a mismatch). They never affect an exact identifier match, which always returns confidence_score: 1.0.

Response changes

The biggest change is the envelope. Below is the same multi-list screening in both formats, side by side.
{
  "job_id": "abc-123-def",
  "status": "COMPLETED",
  "screened_entity": {
    "name": "John Doe",
    "entity_type": "individual"
  },
  "results": {
    "ofac": {
      "list_name": "ofac",
      "list_display_name": "OFAC Sanctions List",
      "status": "CHECKED",
      "has_matches": true,
      "match_count": 1,
      "highest_risk_level": "HIGH",
      "matches": [
        {
          "match_id": "m-1",
          "match_type": "FUZZY",
          "confidence_score": 0.95,
          "risk_level": "HIGH",
          "matched_field": "name",
          "matched_entity": {
            "entity_id": "12345",
            "name": "JOHN DOE",
            "entity_type": "individual",
            "aliases": ["Johnny Doe"],
            "nationality": "US"
          }
        }
      ]
    }
  },
  "screening_summary": {
    "total_lists_checked": 1,
    "total_matches": 1,
    "lists_with_matches": ["ofac"],
    "overall_risk_level": "HIGH",
    "recommended_action": "REVIEW - Manual review required before proceeding.",
    "requires_manual_review": true
  }
}

Field mapping

Legacy pathv2 path
status == "COMPLETED"status == "success"
job_idmeta.request_id
completed_atmeta.timestamp
screened_entity.namedata.subject.full_name
results[list].has_matchesdata.results[list].is_match
results[list].match_countdata.results[list].match_count
results[list].matches[].matched_entitydata.results[list].matches[].entity
results[list].matches[].confidence_scoredata.results[list].matches[].confidence_score
results[list].matches[].match_typedata.results[list].matches[].match_type
results[list].matches[].risk_level(derive from confidence_score)
screening_summarydata.summary
screening_summary.overall_risk_leveldata.summary.overall_risk_level
screening_summary.recommended_actiondata.summary.recommended_action
Single-list responses put the result fields directly under data (data.is_match, data.matches, data.matched_field) instead of under data.results[list]. Multi-list responses use data.results keyed by list name. See List Check for full schemas.

Risk and decisioning

The v2 API exposes risk at two layers:
  1. Per match — confidence_score (0.0–1.0): the intensity of an individual hit. An exact identifier match is always 1.0. Name matches are scored by the search engine and refined by nationality / birth_date.
  2. Aggregate — summary.overall_risk_level + summary.recommended_action: a consolidated assessment across all checked lists, plus a suggested action.
If your legacy integration read per-match risk_level, that field is not returned per match in v2. Either consume the aggregate summary.overall_risk_level, or derive a per-match level from confidence_score using thresholds that fit your risk appetite:
def risk_level_from_score(score: float) -> str:
    if score >= 0.75:
        return "CRITICAL"   # exact / very strong match
    if score >= 0.30:
        return "HIGH"
    if score >= 0.15:
        return "MEDIUM"
    return "LOW"
The thresholds above mirror the platform defaults, but you own the final decision policy (block / review / proceed). Tune the cutoffs to your compliance requirements.

Error handling

Errors also move into the standard envelope.
{
  "status": "FAILED",
  "error_message": "Subject must have at least one identifier or name"
}
SituationLegacyv2
Success flagstatus == "COMPLETED"status == "success"
Failure flagstatus == "FAILED"status == "error"
Error detailerror_message (string)error.code + error.message
Auth failuregateway error401 (missing/invalid token) / 403 (insufficient permission)
See List Check → Error Responses and Authentication → Error Responses for the full catalog.

Migration checklist

1

Update endpoint and base URL

POST /kyc on the staging host first.
2

Move to Bearer auth

Use an API key or JWT; drop the legacy credential and any body-level tenant_id.
3

Rename match_type → search_type

If applicable; keep your existing value.
4

Re-map the response

Read data / meta; switch has_matches → is_match, matched_entity → entity, job_id → meta.request_id.
5

Re-point risk logic

Use summary.overall_risk_level / recommended_action, and derive per-match level from confidence_score if needed.
6

Update error handling

Branch on status and read error.code / error.message; handle 401 / 403.
7

Validate on staging, then cut over

Run representative cases against staging, compare to legacy output, then switch the production base URL.

FAQ

Minimally. The subject object is backward compatible. The only common changes are renaming match_type to search_type and removing any tenant_id from the body.
Yes — the screening engine and lists are the same. Identifier matching is more forgiving (value-first), so you may see exact ID hits that previously required the correct document_type. Name-based scoring is unchanged.
The aggregate risk now lives in summary.overall_risk_level and summary.recommended_action. Each match exposes confidence_score; derive a per-match level from it if your UI needs one.
Yes. Point a copy of your traffic at the v2 staging endpoint, compare outputs, and cut over once parity is confirmed. There is no forced switch on the request side.
You need a v2 API key (or JWT) for Bearer auth. Contact your account administrator to provision an API key with the kyc:create permission.

Next steps

List Check reference

Full v2 request/response schemas, lists, and examples.

Authentication

Bearer tokens, API keys, and the permission model.

API Overview

Environments, rate limits, and platform basics.

Validate a Person or Entity

End-to-end screening walkthrough on v2.